Chapter 3. IPFW Rules

The manual page for ipfw(8) lists the entire command syntax including those for general ruleset construction. We will look first at basic traffic rules and study the more complex capabilities as we progress. Each section will build on the previous sections so that you will be better positioned to understand the advanced material later in the book.

Almost all the examples in this section can be run on the architecture used in the previous chapter, specifically that of Chapter 2, Figure 1. The Quick Start section in the Introduction can provide guidance on setting this back up. Ensure you have the addressing set as shown in the above Chapter 2, Figure 1.

ipfw rules take the general format of:

# ipfw command [rule_number] [set set_number] [prob match_probability] action [log [logamount number]] [altq queue] [{tag | untag} number] body

Bolded keywords indicate literal option text that is added to a rule. Italicized keywords indicate a block of additional content that is rule dependent.

The rule body has its own syntax format:

[ proto from src to dst ] [ options ]

Here is an example with basic syntax for an entire rule:

Description of *ipfw* Rule Syntax
Figure 1. Description of ipfw Rule Syntax
  1. The FreeBSD ipfw shell command

  2. The ipfw rule command (add)

  3. The optional rule number (1000)

  4. An optional set keyword and value (set 2)

  5. An optional probability keyword and value (prob 0.5)

  6. The rule action keyword (deny)

  7. An optional log keyword (log)

  8. An optional logamount keyword and value (logamount 50000)

  9. An optional altq keyword and value (altq red)

  10. An optional tag keyword and value (tag 27)

  11. The body of the rule starting with a protocol keyword (tcp)

  12. A source direction keyword (from)

  13. A source address (any internet address)

  14. A destination keyword (to)

  15. A destination address (any interface on the local system)

  16. A destination port number (8081)

  17. A comment

The example in Figure 1 would not actually load - there is no altq(9) queue named red set up yet - but it does show the overall format of how rules are constructed. This example also shows the use of the Unix line continuation convention using a single backslash at the end of the line (with no following spaces) to continue to the next line.

The essence of ipfw rule processing is found when patterns that are defined in the rule body are matched on incoming or outgoing packets, one at a time and the action keywords are processed in turn.

In other words, ipfw directs traffic flow by first matching each incoming or outgoing packet against patterns supplied within the body of the rule. The patterns include protocols (tcp, udp, igmp, eigrp, etc.), source and destination addresses and ports, and options that apply to the context of the traffic.

3.1. Practical Ruleset Development

In this section, we will concentrate on the basic commands and actions.

The basic command keywords are these:

  • enable/disable - commands to disable or enable ipfw rule processing. The kernel module remains loaded - the effect is to suspend or resume rule processing. This is important to understand early as these commands function like an 'on/off' switch to firewall operation.

  • add - adds a rule.

  • delete - deletes a rule. The rule number must be specified - for example, ipfw delete 1000.

  • list - lists the contents of the current ruleset. Even if there have been no rules added, the list command should always list out the default rule, by default, 65535 deny ip from any to any.

  • show - similar to the list command, show includes counters for each rule matched.

  • flush - delete all the rules in the ruleset except for rules in set 31. (Sets are described later in this Chapter.) Since this is a command with enormous impact, a Yes/No prompt is issued before continuing.

The basic traffic flow action keywords are these:

  • allow | accept | pass | permit - direct ipfw to allow a packet through this rule should the packet match the rule body.

  • count - increment a counter applied to a rule. No other processing is applied to the packet.

  • deny | drop - do not allow a packet to pass through this rule should the packet match the rule body.

  • check-state [:flowname | :any] - check if a dynamic rule already exists.

  • reset - resets Network Address Translation tables.

The ipfw(8) man page has the complete list of action keywords, and describes each in detail.

We will also examine these keywords in the rules section:

  • prob - assign a probability (a value between 0 and 1) to the rule action.

  • set - use a collection of rules.

  • tag and untag - apply an internal tag to a packet affected by the rule.

  • log and logamout - log keywords.

  • reset - send a TCP reset on connection

  • tee - cause packets to flow in multiple ways

  • unreach - specify an action if a packet’s destination is unreachable

  • setdscp - set DiffServe parameters for outbound packets

  • skipto - jump around a ruleset

  • divert - pull packets into userspace for programmatic purposes

  • limit - limit the number of active connections

  • call and return - another way to jump around a ruleset

  • lookup tables and the lookup - log keywords.

Most action keywords, such as allow or deny, determine traffic flow. It is important to become familiar with these actions as they will be used in almost every rule. In addition, carefully note what ipfw does after it matches a packet and applies an action - it either terminates its search, or it goes on to the next rule.

Other action keywords perform an activity that does not have any impact on traffic flow. For example, the count action simply updates counters that apply to a rule. It has no effect on traffic flow and ipfw continues processing with the next rule.

Recall that ipfw is a command line program that uses all the words on the command line as parameters. In developing rules, remember that certain constructs such as braces ({,}), brackets ([,]) and even parentheses themselves are all recognized by the shell and must be escaped with a backslash '\'. The ipfw(8) man page has additional caveats on rule syntax.

We start with the smallest possible ruleset that permits the external1 VM to make a TCP connection to the a service running on the firewall VM.

Start up the external1 VM, and the firewall VM. Load the ipfw.ko kernel module and start the tserv.sh service on the firewall VM to listen for incoming connections. Then create the following ruleset on the firewall VM:

# ipfw add 100 check-state
00100 check-state :default
# ipfw add 1000 allow tcp from any to me 5656 in via em0 setup keep-state
01000 allow tcp from any to me 5656 in via em0 setup keep-state :default

You can test this ruleset immediately by again running the tserv.sh on the firewall VM and the tcon.sh script on the external1 VM as described in the previous chapter. The connection should succeed.

Rule 100 contains the check-state option. It checks to see of a connection is already established and a dynamic rule is in place. If so, any additional packets matching the dynamic rule would be passed.

Rule 1000 contains the add command. This command inserts the requested rule into the ipfw ruleset where it can process packets against the specified actions in the rule body.

It uses the setup and keep-state options to create a dynamic rule for the connection.

In a stateful firewall like ipfw, once a connection from an external host to an internal host is established, the firewall creates a dynamic rule permitting continued traffic along this path until the connection is reset.

Note that without the check-state keyword, no check for a dynamic rule is performed and without the keep-state, no creation of a dynamic rule is performed.

Consider this ruleset with just a single rule:

# ipfw add 1000 allow tcp from any to me 5656

This rule looks like it should work, but it does not. A TCP packet entering ipfw has no pre-existing dynamic rule. Further, the rule does not create one. And, because there is no corresponding rule for outbound traffic, no TCP 3-way handshake is ever completed. Note that a SYN packet is received by the firewall, but not by the destination service.

If we add the rule:

# ipfw add 2000 allow tcp from me to any

the TCP 3-way handshake is allowed to complete and the data is sent from the external1 VM host to the tserv.sh process running on the firewall VM.

While this method works, it uses two rules instead of one. In this case, the better solution is to use the keep-state and check-state options early in the ruleset as shown in the original example in this section.

3.2. Dynamic Rules

So, what exactly are "dynamic rules"? The scripts we are using close the TCP connection each time, so the dynamic rules are short lived, and cannot be easily examined.

To see dynamic rules in action, manually set up a Netcat listener on the firewall host and send data with a Netcat sender on the external VM host:

On the firewall VM, start up the listener service manually:

# ncat -l 203.0.113.50 5656

Then, on the External1 VM, use ncat to connect to the service on the firewall and type a message:

# ncat 203.0.113.50 5656
hello there

The message should appear on the console of the firewall VM. If it does not, ensure that the original rule from the previous section is active.

Manually Creating Traffic to Examine Dynamic Rules
Figure 2. Manually Creating Traffic to Examine Dynamic Rules

Figure 2 shows the connection is open between the external1 and firewall VMs.

While the connection is still open, run the following command on the firewall VM serial console:

Viewing Dynamic Rules
Figure 3. Viewing Dynamic Rules

You should see output similar to that in Figure 3.

The -d option displays dynamic rules in addition to regular rules. The -D option displays just dynamic rules.

3.2.1. Notes on Rule Numbering

Each rule is assigned a rule number, even if you do not specify one. The details for rule number handing are found in the ipfw(8) man page. Note that rules are assigned numbers in increments specified by the sysctl net.inet.ip.fw.autoinc_step

# sysctl net.inet.ip.fw.autoinc_step
net.inet.ip.fw.autoinc_step: 100

We start with a simple check-state rule and see that ipfw has assigned a number associated with the increment sysctl shown above:

# ipfw add check-state
00000 check-state :default

# ipfw list
00100 check-state :default
65535 deny ip from any to any00000 check-state :default

ipfw has automatically assigned the rule number 100. While it can be convenient to have ipfw add a rule number automatically, you should always assign rule numbers yourself. This ensures that you consciously put a rule in a specific place within the ruleset. With large rulesets this is critical. A rule automatically assigned by ipfw can be placed where it can have an unexpected effect.

Consider this ruleset:

# ipfw list
00300 deny ip from any to 200.200.200.200
00400 deny ip from any to 200.200.200.201
00500 deny ip from any to 200.200.200.202
00600 deny ip from any to 200.200.200.203
00700 deny ip from any to 200.200.200.204
00800 deny ip from any to 200.200.200.205
00800 deny ip from any to 200.200.200.206
65535 deny ip from any to any

Having forgotten to add the check-state rule you quickly add it:

# *ipfw add check-state*
00000 check-state :default

But the result may not be what you intended:

# ipfw list
00300 deny ip from any to 200.200.200.200
00400 deny ip from any to 200.200.200.200
00500 deny ip from any to 200.200.200.200
00600 deny ip from any to 200.200.200.200
00700 deny ip from any to 200.200.200.200
00800 deny ip from any to 200.200.200.200
00800 deny ip from any to 200.200.200.200
00900 check-state :default
65535 deny ip from any to any

ipfw allows rules with the same rule number to be added to the ruleset, and it will keep track of the rules in the order they were entered. This is easy to forget when manually entering rules from the command line and using command line editing to change something simple like the last byte of an IP address.

It is important to remember that all such rules are affected by commands that operate on one or more lines, such as the delete command:

# ipfw add 100 check-state
00100 check-state :default
# ipfw add 1000 allow tcp from 203.0.113.10 to me 5656 keep-state
01000 allow tcp from 203.0.113.10 to me 5656 keep-state :default
# ipfw add 1000 allow tcp from 203.0.113.20 to me 5656 keep-state
01000 allow tcp from 203.0.113.20 to me 5656 keep-state :default
# ipfw add 1000 allow tcp from 203.0.113.30 to me 5656 keep-state
01000 allow tcp from 203.0.113.30 to me 5656 keep-state :default
# ipfw add 1000 allow tcp from 203.0.113.40 to me 5656 keep-state
01000 allow tcp from 203.0.113.40 to me 5656 keep-state :default
#
# ipfw list
00100 check-state :default
01000 allow tcp from 203.0.113.10 to me 5656 keep-state :default
01000 allow tcp from 203.0.113.20 to me 5656 keep-state :default
01000 allow tcp from 203.0.113.30 to me 5656 keep-state :default
01000 allow tcp from 203.0.113.40 to me 5656 keep-state :default
65535 deny ip from any to any
#
# ipfw delete 1000
#
# ipfw list
00100 check-state :default
65535 deny ip from any to any

The delete command can also process both ranges and lists of rules.

Consider the following ruleset:

# ipfw list
00100 check-state :default
01000 allow tcp from 203.0.113.10 to me 5656 keep-state :default
01100 allow tcp from 203.0.113.11 to me 5656 keep-state :default
01200 allow tcp from 203.0.113.12 to me 5656 keep-state :default
01300 allow tcp from 203.0.113.13 to me 5656 keep-state :default
02000 allow tcp from 203.0.113.20 to me 5656 keep-state :default
02100 allow tcp from 203.0.113.21 to me 5656 keep-state :default
02200 allow tcp from 203.0.113.22 to me 5656 keep-state :default
02300 allow tcp from 203.0.113.23 to me 5656 keep-state :default
03000 allow tcp from 203.0.113.30 to me 5656 keep-state :default
03100 allow tcp from 203.0.113.31 to me 5656 keep-state :default
03200 allow tcp from 203.0.113.32 to me 5656 keep-state :default
03300 allow tcp from 203.0.113.33 to me 5656 keep-state :default
04000 allow tcp from 203.0.113.40 to me 5656 keep-state :default
04100 allow tcp from 203.0.113.41 to me 5656 keep-state :default
04200 allow tcp from 203.0.113.42 to me 5656 keep-state :default
04300 allow tcp from 203.0.113.43 to me 5656 keep-state :default
04400 allow tcp from 203.0.113.44 to me 5656 keep-state :default
04500 allow tcp from 203.0.113.45 to me 5656 keep-state :default
04600 allow tcp from 203.0.113.46 to me 5656 keep-state :default
04700 allow tcp from 203.0.113.47 to me 5656 keep-state :default
04800 allow tcp from 203.0.113.48 to me 5656 keep-state :default
04900 allow tcp from 203.0.113.49 to me 5656 keep-state :default
65535 deny ip from any to any

A range is specified by two number separated by a dash: for example 5000-7350; whereas a list is a space-separated collection of numbers on the command line.

We wish to delete rules from 1000 to 2999 and all odd-numbered rules between 4000 and 5000:

# ipfw delete 1000-2999 4100 4300 4500 4700 4900
#
# ipfw list
00100 check-state :default
03000 allow tcp from 203.0.113.30 to me 5656 keep-state :default
03100 allow tcp from 203.0.113.31 to me 5656 keep-state :default
03200 allow tcp from 203.0.113.32 to me 5656 keep-state :default
03300 allow tcp from 203.0.113.33 to me 5656 keep-state :default
04000 allow tcp from 203.0.113.40 to me 5656 keep-state :default
04200 allow tcp from 203.0.113.42 to me 5656 keep-state :default
04400 allow tcp from 203.0.113.44 to me 5656 keep-state :default
04600 allow tcp from 203.0.113.46 to me 5656 keep-state :default
04800 allow tcp from 203.0.113.48 to me 5656 keep-state :default
65535 deny ip from any to any

Note that the delete command will operate on comma separated values, but the delete command will only remove the first value in a comma separated list which is usually not what you intend. The command does not throw an error, but it does not delete all the lines requested.

# ipfw delete 3100,3200,3300
# echo $?
0                     <---  No error found with the previous command.
#
# ipfw list
00100 check-state :default
03000 allow tcp from 203.0.113.30 to me 5656 keep-state :default
03200 allow tcp from 203.0.113.32 to me 5656 keep-state :default
03300 allow tcp from 203.0.113.33 to me 5656 keep-state :default
04000 allow tcp from 203.0.113.40 to me 5656 keep-state :default
04200 allow tcp from 203.0.113.42 to me 5656 keep-state :default
04400 allow tcp from 203.0.113.44 to me 5656 keep-state :default
04600 allow tcp from 203.0.113.46 to me 5656 keep-state :default
04800 allow tcp from 203.0.113.48 to me 5656 keep-state :default
65535 deny ip from any to any

The show command is similar to the list command but it also includes packet count and byte count for each rule. Consider the following ruleset on the firewall VM:

# ipfw list
00100 check-state :default
01000 allow udp from 203.0.113.10 to me 5656
02000 allow udp from 203.0.113.10 to me 5657
03000 allow udp from 203.0.113.10 to me 5658
04000 allow udp from 203.0.113.10 to me 5659
65535 deny ip from any to any

On the external1 VM, run the uconr.sh script to send packets to ports 5656, 5657, and 5658, randomly:

# sh uconr.sh 5656 1
PORT1     = [5656]
SLEEPVAL = [1]
UDP packet from [203.0.113.10],[5656],[1]
UDP packet from [203.0.113.10],[5656],[2]
UDP packet from [203.0.113.10],[5658],[3]
UDP packet from [203.0.113.10],[5657],[4]
UDP packet from [203.0.113.10],[5659],[5]
UDP packet from [203.0.113.10],[5659],[6]
UDP packet from [203.0.113.10],[5659],[7]
UDP packet from [203.0.113.10],[5656],[8]
UDP packet from [203.0.113.10],[5658],[9]
UDP packet from [203.0.113.10],[5659],[10]
UDP packet from [203.0.113.10],[5658],[11]
UDP packet from [203.0.113.10],[5656],[12]
UDP packet from [203.0.113.10],[5656],[13]
UDP packet from [203.0.113.10],[5656],[14]
UDP packet from [203.0.113.10],[5659],[15]
UDP packet from [203.0.113.10],[5656],[16]
UDP packet from [203.0.113.10],[5657],[17]
UDP packet from [203.0.113.10],[5659],[18]
UDP packet from [203.0.113.10],[5659],[19]
UDP packet from [203.0.113.10],[5657],[20]
UDP packet from [203.0.113.10],[5659],[21]
^C

Running the ipfw show command outputs:

# ipfw show
00100  0   0 check-state :default
01000  7 494 allow udp from 203.0.113.10 to me 5656
02000  3 212 allow udp from 203.0.113.10 to me 5657
03000  3 211 allow udp from 203.0.113.10 to me 5658
04000  8 565 allow udp from 203.0.113.10 to me 5659
65535 11 897 deny ip from any to any

The output shows the number of packets and the number of bytes processed by each rule, including the default rule which may have processed many more packets.

Note that the -D command parameter will show counts for dynamic rules similar to the above.

# ipfw -D show
Dynamic rules (2 288):
04000 2 100 (1s) STATE tcp 203.0.113.10 31662 <-> 203.0.113.50 5659 :default
03000 8 482 (1s) STATE tcp 203.0.113.10 45732 <-> 203.0.113.50 5658 :default

This is a useful tool for debugging. Paired with the zero command which can clear counters with precise rule selection, it can show what rules are still processing a rule match.

The zero command takes a space separated list of rules (similar to the delete command) to clear counters. However, unlike the delete command, ranges (e.g 2000-3000) are not allowed.

# ipfw zero 2000 3000
#
# ipfw show
00100  0   0 check-state :default
01000  7 494 allow udp from 203.0.113.10 to me 5656
02000  0   0 allow udp from 203.0.113.10 to me 5657
03000  0   0 allow udp from 203.0.113.10 to me 5658
04000  8 565 allow udp from 203.0.113.10 to me 5659
65535 11 897 deny ip from any to any

Clearing all rule match counters can be done with ipfw zero with no parameters.

Counters are also a feature of rules that specify the log keyword. We will see an example of this when we explore the log and logamount keywords later.

3.3. Keywords

3.3.1. Protocols

Protocols are those defined by IANA - the Internet Assigned Numbers Authority (https://www.iana.org) and are included in Unix systems in /etc/protocols. From this file, we can learn what numbers are assigned to common (and some very obscure) protocols - ip (0), tcp (6), udp (17), icmp(1), and many others.

Source and destination protocols can be the conventional IP or IPv6 addresses. However, the ipfw(8) manual page has this more detailed explanation:

"The first part (proto from src to dst) is for backward compatibility with earlier versions of FreeBSD. In modern FreeBSD any match pattern (including MAC headers, IP protocols, addresses and ports) can be specified in the options section."

The ipfw keywords for common protocols include:

ip4 | ipv4 Matches IPv4 packets.

ip6 | ipv6 Matches IPv6 packets.

ip | all Matches any IP packet.

The logical operator "or" can be use to combine multiple protocols where any one of them applies. The "{" and "}" braces can be used to group "or" conditions (known as "or-blocks"). Only one level of braces can be used. Braces must be escaped with a backslash '\' to prevent them from being interpreted directly by the command line shell:

# ipfw add 1100 deny \{ tcp or udp or eigrp or chaos \} from 1.2.3.4 to 5.6.7.8
01100 deny { tcp or udp or eigrp or chaos } from 1.2.3.4 to 5.6.7.8

Consider this ruleset in a shell script:


#!/bin/sh

ipfw add 5000 deny \{ icmp or ip or igmp or ggp or ipencap or st2 or tcp or cbt or egp or igp or bbn-rcc or nvp or pup or argus or emcon or xnet or chaos or udp or mux or dcn or hmp or prm or xns-idp or trunk-1 or trunk-2 or leaf-1 or leaf-2 or rdp or irtp or iso-tp4 or netblt or mfe-nsp or merit-inp or dccp or 3pc or idpr or xtp or ddp or idpr-cmtp or tp++ or il or ipv6 or sdrp or ipv6-route or ipv6-frag or idrp or rsvp or gre or dsr or bna or esp or ah or i-nlsp or swipe or narp or mobile or tlsp or skip or ipv6-icmp or ipv6-nonxt or ipv6-opts or cftp or sat-expak or kryptolan or rvd or ippc or sat-mon or visa or ipcv or cpnx or cphb or wsn or pvp or br-sat-mon or sun-nd or wb-mon or wb-expak or iso-ip or vmtp or secure-vmtp or vines or ttp or nsfnet-igp or dgp or tcf or eigrp or ospf or sprite-rpc or larp or mtp or ax.25 or ipip or micp or scc-sp or etherip or encap or gmtp or ifmp or pnni or pim or aris or scps or qnx or a/n or ipcomp or snp or compaq-peer or ipx-in-ip or carp or pgm or l2tp or ddx or iatp or stp or srp or uti or smp or sm or ptp or isis or fire or crtp or crudp or sscopmce or iplt or sps or pipe or sctp or fc or rsvp-e2e-ignore or mobility-header or udplite or mpls-in-ip or manet or hip or shim6 or wesp or rohc or pfsync or divert \} from any to me

exit


Note that the above file is shown as one very long line and does not use the Unix line continuation convention.

This command will deny all traffic using all protocols defined in /etc/protocols. The above command will complete successfully. However, due to a bug in the "or-block" parser, you cannot list the "ip" protocol first. If you swap the first two protocols - icmp and ip - the command throws an error.

For example,

# ipfw add 1000 deny \{ igmp or ip or eigrp \} from any to me

works ok but

# ipfw add 1000 deny \{ ip or igmp or eigrp \} from any to me
ipfw: invalid OR block

fails.

The use of the logical "and" operator in a protocol block is an error. A packet can be in only one protocol at a time. However, the use of the logical "not" operator is permitted in front of a protocol identifier:

# ipfw add 1000 deny \{ icmp  or not igmp \} from any to me

Careful consideration of all logical conditions is essential to correct operation of a ruleset.

In later versions of FreeBSD, the use of the protocol "or-block" is noted as deprecated in ipfw(8) but the operation may still complete successfully until the feature is removed completely.

3.3.2. Addresses

Source and destination addresses can be any of the following:

  • any - matches any IP address. We have already seen many examles of this keyword.

  • me - matches any IP address configured on an interface in the system.

Note that the interface does not have to have an IP or IPv6 address, nor does it have to be up or even exist at the time the rule is entered. Thus, be aware that a rule with keyword me may affect traffic on interfaces that are configured at a later time. Consider the following system interface list:

# ifconfig -a
em0: flags=8863<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=481209b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,WOL_MAGIC,VLAN_HWFILTER,NOMAP>
        ether 02:49:50:46:57:41
        inet 203.0.113.50 netmask 0xffffff00 broadcast 203.0.113.255
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
        inet 127.0.0.1 netmask 0xff000000
        groups: lo

The following attempt to add a rule with a non-existent interface succeeds even though there is no wlan0 interface defined:

# ipfw add 1000 deny tcp from 1.2.4.4 to me via wlan0
01000 deny tcp from 1.2.4.4 to me via wlan0
  • me6 - matches any IPv6 address similarly to the above me keyword.

  • table(name[,value]) - matches any IPv4 or IPv6 address for an entry in the named table.

Tables are discussed below in Lookup Tables.

IPv4 and IPv6 addresses follow the usual conventions regarding address and mask length. Addresses can also be grouped into a list, similar to the capability discussed for protocols above.

# ipfw add 2000 allow tcp from \{ 2.3.4.5/32, 3.4.5.0/24, 10.0.0.0/8 \} to me

# ipfw add 2000 allow tcp from \{ 2607:fcc0:0:35::dd/64, 2608:abcd:0:2300::9eac/64  \} to me

However, you cannot mix IPv4 and IPv6 addresses in the same list. Use two different rules instead.

For sparse collections of addresses, consider the alternate form allowed by

addr-set: addr[/masklen]{list}

such as:

# ipfw add 1000 allow tcp from 1.2.3.0/24\{128,9,35-45,7\} to me

In this form, lists and increasing ranges are allowed. ipfw will consolidate overlapping ranges, and will reorder the list in the display to show increasing addresses from left to right. Note that spaces are only allowed after commas between list elements, nowhere else.

This form does not work due to incorrect placement of spaces:

# ipfw add 1000 allow tcp from 1.2.4.0/24\{ 128,9,25-45,7-25 \} to me
ipfw: missing ``to''

This form works by correcting where spaces occur on the command line.

# ipfw add 1000 allow tcp from 1.2.4.0/24\{128,9,25-45,7-25\} to me
01000 allow tcp from 1.2.4.0/24{7-45,128} to me

As noted, ipfw will simplify, reorder, and display the list:

# ipfw list
01000 allow tcp from 1.2.4.0/24{7-45,128} to me
65535 deny ip from any to any

Note that ranges must be defined as increasing. Also, as noted in ipfw(8), there is no support for sets of IPv6 addresses.

3.3.3. Ports

Ports may be specified by number or by service name. Service names, also under the provenance of IANA, are found in /etc/services on Unix systems.

Ports can be specified as individual items, lists, or ranges. Typically ports are used to determine a destination service and so apply to the destination address (although source ports are also permitted):

# ipfw add 1000 allow tcp from 1.2.3.4 to me daytime
01000 allow tcp from 1.2.3.4 to me 13

# ipfw add 2000 allow tcp from 2.3.4.5 to me ssh, telnet, smtp
02000 allow tcp from 2.3.4.5 to me 22,23,25

# ipfw add 3000 allow tcp from 3.4.5.6 to me auditd-domain
03000 allow tcp from 3.4.5.6 to me 48-53

General Notes on Port Ranges

  1. A range such as that shown above (auditd-domain) may accidentally include ports you do not want. In this case, ports tacacs (49), re-mail-ck (50), la-maint (51), and xns-time (52) would be included. Even if this is really what you want, you should always check actual port numbers when using named ranges for ports.

  2. Some service names include the dash character '-' as part of the name, as in the point above. In these cases you will have to escape the dash with a double backslash, one for the shell and one for ipfw:

    # ipfw add 4000 allow tcp from 4.5.6.7 to me ftp, ftp\\-data
    04000 allow tcp from 4.5.6.7 to me 21,20
  3. Some applications require a range of source and destination ports in both directions. This is easy to accomplish with ranges and a keep-state rule:

    # ipfw add 1000 allow tcp from 203.0.113.10 5200-5205 to me 5656-5658 keep-state
    01000 allow tcp from 203.0.113.10 5200-5205 to me 5656-5658 keep-state :default
    #
    # ipfw list
    01000 allow tcp from 203.0.113.10 5200-5205 to me 5656-5658 keep-state :default
    65535 deny ip from any to any

To test common ports in both directions we have to manually connect using Netcat so we can set up the source and destination ports as we need them:

#
# ipfw list
01000 allow tcp from 203.0.113.10 5200-5205 to me 5656-5658 keep-state :default
65535 deny ip from any to any

Note: the transmission and reception lines have been aligned on each side.

Manually Testing Common Ports in Both Directions
Figure 4. Manually Testing Common Ports in Both Directions

Note that no connection was achieved when the destination port was out of bounds (5659) and when the source port was out of bounds (5206).

3.3.4. Prob

The prob keyword is used to assign a chance, that is a probability (a floating point value between 0 and 1), that an incoming packet will be matched. If the chance is successful, the corresponding rule performs the required action. If the chance is not successful, the rule is not matched and rule processing continues to the next rule.

To test this keyword, we use "sh ucont.sh 5656 1" script on the external1 VM’s side to repeatedly send a UDP packet to the firewall VM, who is listening on UDP port 5656. Using the prob keyword, we set a probability of .5 (a 50% chance) that the packet will be matched. The action is to let the packet pass to the service, which just prints the contents of the packet.

As you can see below, there were 24 out of 50 packets received, very close to 50% for such a small sample:

#
# ipfw add 3000 prob 0.5 allow udp from any to me 5656
03000 prob 0.500000 allow udp from any to me 5656
#
# ipfw list
03000 prob 0.500000 allow udp from any to me 5656
65535 deny ip from any to any
#
# sh userv.sh 5656
PORT1 = [5656]
Starting UDP listener on [203.0.113.50],[5656]
UDP packet from [203.0.113.10],[5656],[5]
UDP packet from [203.0.113.10],[5656],[6]
UDP packet from [203.0.113.10],[5656],[7]
UDP packet from [203.0.113.10],[5656],[10]
UDP packet from [203.0.113.10],[5656],[11]
UDP packet from [203.0.113.10],[5656],[13]
UDP packet from [203.0.113.10],[5656],[14]
UDP packet from [203.0.113.10],[5656],[16]
UDP packet from [203.0.113.10],[5656],[19]
UDP packet from [203.0.113.10],[5656],[20]
UDP packet from [203.0.113.10],[5656],[22]
UDP packet from [203.0.113.10],[5656],[28]
UDP packet from [203.0.113.10],[5656],[29]
UDP packet from [203.0.113.10],[5656],[32]
UDP packet from [203.0.113.10],[5656],[33]
UDP packet from [203.0.113.10],[5656],[34]
UDP packet from [203.0.113.10],[5656],[35]
UDP packet from [203.0.113.10],[5656],[37]
UDP packet from [203.0.113.10],[5656],[38]
UDP packet from [203.0.113.10],[5656],[39]
UDP packet from [203.0.113.10],[5656],[41]
UDP packet from [203.0.113.10],[5656],[43]
UDP packet from [203.0.113.10],[5656],[45]
UDP packet from [203.0.113.10],[5656],[49]
^C#

3.3.5. Sets

Firewall rules can be grouped into different sets, and you can switch between sets atomically. Why would you want this feature? Consider a datacenter with two sets of identical servers on separate networks. You want to take down one set for maintenance, but first you want to transfer traffic to the other set of servers. Bingo - you have a solution.

Sets are useful, but they do come with some caveats which we will describe throughout this section.

The default set is set 0. Let’s create rules in set 0 and 1 with slight differences between the two. Note that this example also shows the use of the ipfw comment feature - allowing comments on a per-rule basis.

# ipfw add 1000 set 0 check-state
01000 check-state :default
#
# ipfw add 1100 set 0 allow tcp from any to me 5656 setup keep-state // 5656 only
01100 allow tcp from any to me 5656 setup keep-state :default // 5656 only
#
# ipfw add 2000 set 1 check-state
02000 check-state :default
#
# ipfw add 2100 set 1 allow tcp from any to me 5657 setup keep-state // 5657 only
02100 allow tcp from any to me 5657 setup keep-state :default // 5657 only
#
# ipfw set show
enable 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

The above ipfw set show command lists all enabled sets, here showing both 0 and 1 as enabled.

From the external1 VM, commence communications using the tcpcon.sh script as shown below. The first two communications ([1], [2]) are to port 5656, the next two communications are to port 5657 ([3],[4]). The firewall host shows all four communications received:

Use of Sets
Figure 5. Use of Sets

Right before communication 5, the firewall host admin disabled set 0, (# ipfw set disable 0) effectively blocking access to port 5656. Disabling set 0, effectively removes the rules for port 5656 and so the communications [5] and [6] fail. The external1 VM goes back to port 5657 for communications [7] and [8], which are successful.

Notice that if you list the firewall ruleset with just ipfw list, only the sets that are enabled actually show up. If you want to make sure which sets are enabled / disabled, use the -S flag on the ipfw command as shown below.

# ipfw set enable 0
#
# ipfw set show
enable 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# ipfw -S list
01000 set 0 check-state :default
01100 set 0 allow tcp from any to me 5656 setup keep-state :default // 5656 only
02000 set 1 check-state :default
02100 set 1 allow tcp from any to me 5657 setup keep-state :default // 5657 only
65535 set 31 deny ip from any to any
#
# ipfw set disable 0
#
# ipfw set show
disable 0 enable 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# ipfw -S list
# DISABLED 01000 set 0 check-state :default
# DISABLED 01100 set 0 allow tcp from any to me 5656 setup keep-state :default // 5656 only
02000 set 1 check-state :default
02100 set 1 allow tcp from any to me 5657 setup keep-state :default // 5657 only
65535 set 31 deny ip from any to any
#

To atomically change from one set to the other use the ipfw set swap command:

# ipfw set swap 0 1

What actually happens is that the rules in the sets get swapped; that is, all the rules in set 0 get put in set 1 and all the rules in set 1 get put in set 0.

#
# ipfw -S list
# DISABLED 01000 set 0 check-state :default
# DISABLED 01100 set 0 allow tcp from any to me 5656 setup keep-state :default // 5656 only
02000 set 1 check-state :default
02100 set 1 allow tcp from any to me 5657 setup keep-state :default // 5657 only
65535 set 31 deny ip from any to any
#
# ipfw set swap 0 1
#
# ipfw -S list
01000 set 1 check-state :default
01100 set 1 allow tcp from any to me 5656 setup keep-state :default // 5656 only
# DISABLED 02000 set 0 check-state :default
# DISABLED 02100 set 0 allow tcp from any to me 5657 setup keep-state :default // 5657 only
65535 set 31 deny ip from any to any

Note carefully that if you swap sets where one of the sets is disabled, the set number is still disabled after the swap, even though the rules are now different. This can lead to unexpected consequences such as the following:

# ipfw -S list
01000 set 0 check-state :default
01100 set 0 allow tcp from any to me 5656 setup keep-state :default // 5656 only
02000 set 1 check-state :default
02100 set 1 allow tcp from any to me 5660 setup keep-state :default // 5660 only
65535 set 31 deny ip from any to any
#
# ipfw set disable 0
#
# ipfw -S list
# DISABLED 01000 set 0 check-state :default
# DISABLED 01100 set 0 allow tcp from any to me 5656 setup keep-state :default // 5656 only
02000 set 1 check-state :default
02100 set 1 allow tcp from any to me 5660 setup keep-state :default // 5660 only
65535 set 31 deny ip from any to any
#
# ipfw set swap 0 1
#
# ipfw -S list
01000 set 1 check-state :default
01100 set 1 allow tcp from any to me 5656 setup keep-state :default // 5656 only
# DISABLED 02000 set 0 check-state :default
# DISABLED 02100 set 0 allow tcp from any to me 5660 setup keep-state :default // 5660 only
65535 set 31 deny ip from any to any

Here set 0 is disabled, and then swapped. After the swap, set 0 is still disabled, though the rules have changed.

Note that all sets are initially enabled. When you disable a set, say set 3, all other sets are still active, even if no rule references them. You can think of sets as the pieces on the back row of a chess board. If you remove a knight or a bishop, that one piece is not able to play, but all the others are able to play.

set 31 cannot be deleted or changed. It can however partially participate in a swap.

# ipfw set swap 1 31

This swap will complete successfully (return code 0), but the effect is not the same as the swaps above. The default rule in set 31 is not swapped, but the set number for the other rules (rules in set 1) are set to 31:

# ipfw -S list
01000 set 1 check-state :default
01100 set 1 allow tcp from 1.2.3.4 to me 8080
01200 set 1 allow tcp from 1.2.3.4 to me 8081
65535 set 31 deny ip from any to any
#
# ipfw set swap 1 31
#
# ipfw -S list
01000 set 31 check-state :default
01100 set 31 allow tcp from 1.2.3.4 to me 8080
01200 set 31 allow tcp from 1.2.3.4 to me 8081
65535 set 31 deny ip from any to any

As noted in the ipfw(8) manual page, rules in set 31 cannot be flushed. We now have 4 rules in set 31:

# ipfw -f flush
Flushed all rules.
#
# ipfw -S list
01000 set 31 check-state :default
01100 set 31 allow tcp from 1.2.3.4 to me 8080
01200 set 31 allow tcp from 1.2.3.4 to me 8081
65535 set 31 deny ip from any to any

We cannot use ipfw flush to clean out non-default rules, but we can use ipfw delete set 31 to clean out all but the default rule, which cannot be changed:

# ipfw delete set 31
#
# ipfw -S list
65535 set 31 deny ip from any to any

Further, you should note that any set you disable remains disabled after a flush. If you disable a set and then flush the ruleset, any rules added back into that set will still be disabled. This includes set 0, the default set.

Consider the following:

# ipfw list
01000 check-state :default
01100 allow tcp from any to me 1111
02000 allow tcp from any to me 2222
03000 allow tcp from any to me 3333
65535 deny ip from any to any
#
# ipfw set disable 0
#
# ipfw -S list
# DISABLED 01000 set 0 check-state :default
# DISABLED 01100 set 0 allow tcp from any to me 1111
# DISABLED 02000 set 0 allow tcp from any to me 2222
# DISABLED 03000 set 0 allow tcp from any to me 3333
65535 set 31 deny ip from any to any
#
# ipfw -f flush
Flushed all rules.
#
# ipfw add 100 check-state
00100 check-state :default
# ipfw add 200 allow tcp from any to me 5555
00200 allow tcp from any to me 5555
# ipfw add 300 allow tcp from any to me 6666
00300 allow tcp from any to me 6666
# ipfw add 400 allow tcp from any to me 7777
00400 allow tcp from any to me 7777
#
# ipfw -S list
# DISABLED 00100 set 0 check-state :default
# DISABLED 00200 set 0 allow tcp from any to me 5555
# DISABLED 00300 set 0 allow tcp from any to me 6666
# DISABLED 00400 set 0 allow tcp from any to me 7777
65535 set 31 deny ip from any to any
#

Because set 0 was disabled before the flush, the flush has no effect on the enable/disable state of that set.

Note that you can even disable sets that do not yet exist:

# kldload ipfw
ipfw2 (+ipv6) initialized, divert loadable, nat loadable, default to deny, logging disabled
#
# ipfw -S list
65535 set 31 deny ip from any to any
#
# ipfw set show
enable 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# ipfw set disable 4 5 6 7 8 9
#
# ipfw set show
disable 4 5 6 7 8 9 enable 0 1 2 3 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#

Using sets can be very helpful, as long as you understand their properties and limitations.

3.3.6. Tags

Tags allow for marking incoming packets in such a way that later rules can be applied based on the tag.

For a simple example, consider tagging incoming packets from different networks. Later rules determine if the tagged packets are allowed or denied:

# ipfw add 100 check-state
00100 check-state :default
#
# ipfw add 1000 count tag 10 tcp from 172.16.200.0/24 to me 5656
01000 count tag 10 tcp from 172.16.200.0/24 to me 5656
#
# ipfw add 1100 count tag 20 tcp from 172.16.225.0/24 to me 5656
01100 count tag 20 tcp from 172.16.225.0/24 to me 5656
#
# ipfw add 1200 count tag 30 tcp from 203.0.113.0/24 to me 5656
01200 count tag 30 tcp from 203.0.113.0/24 to me 5656
#
# ipfw add 3000 allow tcp from any to me tagged 30 setup keep-state
03000 allow tcp from any to me tagged 30 setup keep-state :default
#
# ipfw add 4000 deny tcp from any to me tagged 10,20
04000 deny tcp from any to me tagged 10,20
#

We can test this with manual use of Netcat and using the ncat(1) option to set its own source address. To do this we first setup two alias address on the em0 interface:

# ifconfig em0 172.16.200.10/24 alias
# ifconfig em0 172.16.225.10/24 alias
#
# ifconfig em0
em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
   options=81209b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,WOL_MAGIC,VLAN_HWFILTER>
        ether 02:49:50:46:57:10
        inet 203.0.113.10 netmask 0xffffff00 broadcast 203.0.113.255
        inet 172.16.200.10 netmask 0xffffff00 broadcast 172.16.200.255
        inet 172.16.225.10 netmask 0xffffff00 broadcast 172.16.225.255
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
#
Use of Tags
Figure 6. Use of Tags

Because of the rule tagging in this ruleset, only traffic tagged with value "30" is allowed to pass.

Tags, combined with lookup tables allow for powerful policy based network access.

3.3.7. Logging

ipfw supports two methods of logging:

3.3.7.1. Method 1 – using ipfw0, the ipfw pseudointerface

# kldload ipfw
# ifconfig ipfw0 create

Note that you must have the ipfw.ko kernel module loaded before you can create the ipfw0 interface. Also, note that if you unload ipfw.ko, the interface is destroyed and is no longer available.

Why use the ipfw0 interface?

It is possible to read logs in real time with programs such as tcpdump(1), wireshark(1), or other network monitoring programs.

An example is given further below.

3.3.7.2. Method 2 – use syslogd(8)

Setting the sysctl variable net.inet.ip.fw.verbose = 1 will instruct the firewall to log packets to syslogd(8) even when the ipfw0 interface exists. Syslogd must be configured via /etc/syslog.conf. ipfw packets will be logged with a LOG_SECURITY facility. The logging limit is configurable via net.inet.ip.fw.verbose_limit, which is set to 0 (unlimited) by default.

Why use syslog?

Of the two methods, it is the only one that processes count actions.

To test logging, create a rule with the log keyword:

# ipfw add 1000 allow log tcp from any to me 5656 setup keep-state

Counters are also a feature of rules that specify the log keyword. If we replace the above ruleset and add the log keyword, matches for all rules will be included in the log entries with associated counts.

With the ruleset changes to add the log keyword, our ruleset looks like this:

# ipfw list
00100 check-state :default
01000 allow log tcp from 203.0.113.10 to me 5656 keep-state :default
02000 allow log tcp from 203.0.113.10 to me 5657 keep-state :default
03000 allow log tcp from 203.0.113.10 to me 5658 keep-state :default
04000 allow log tcp from 203.0.113.10 to me 5659 keep-state :default
65535 deny ip from any to any
3.3.7.2.1. Using Method 1

Using Method 1, we capture/view logs with:

# tcpdump -i ipfw0 -X

Note: You can redirect binary data to a 'savefile' with the -w file option.

If you are just reading tcpdump output, experiment with the -v, -vv, and -vvv options, which gives increasingly more verbose output.

The example below examines traffic with the ucont.sh on the external1 VM and the userv3.sh script on the firewall VM.

UDP Traffic From External1 to Firewall
Figure 7. UDP Traffic From External1 to Firewall

Logged traffic from the above communication appears on the log device ipfw0:

#
# tcpdump -i ipfw0 -X -vvv
tcpdump: WARNING: ipfw0: That device doesn't support promiscuous mode
(BIOCPROMISC: Invalid argument)
tcpdump: listening on ipfw0, link-type EN10MB (Ethernet), capture size 262144 bytes
20:50:44.259127 IP (tos 0x0, ttl 64, id 61929, offset 0, flags [none], proto UDP (17), length 70)
    203.0.113.10.27337 > 203.0.113.50.5656: [udp sum ok] UDP, length 42
        0x0000:  4500 0046 f1e9 0000 4011 1c61 ac10 0a0a  E..F....@..a....
        0x0010:  ac10 0a32 6ac9 1618 0032 00ff 5544 5020  ...2j....2..UDP.
        0x0020:  7061 636b 6574 2066 726f 6d20 5b31 3732  packet.from.[172
        0x0030:  2e31 362e 3130 2e31 305d 2c5b 3536 3536  .16.10.10],[5656
        0x0040:  5d2c 5b31 5d0a                           ],[1].
20:50:44.581025 IP (tos 0x0, ttl 64, id 61930, offset 0, flags [none], proto UDP (17), length 70)
    203.0.113.10.41914 > 203.0.113.50.5656: [udp sum ok] UDP, length 42
        0x0000:  4500 0046 f1ea 0000 4011 1c60 ac10 0a0a  E..F....@..`....
        0x0010:  ac10 0a32 a3ba 1618 0032 c80c 5544 5020  ...2.....2..UDP.
        0x0020:  7061 636b 6574 2066 726f 6d20 5b31 3732  packet.from.[172
        0x0030:  2e31 362e 3130 2e31 305d 2c5b 3536 3536  .16.10.10],[5656
        0x0040:  5d2c 5b32 5d0a                           ],[2].
20:50:45.960845 IP (tos 0x0, ttl 64, id 61931, offset 0, flags [none], proto UDP (17), length 70)
    203.0.113.10.33126 > 203.0.113.50.5656: [udp sum ok] UDP, length 42
        0x0000:  4500 0046 f1eb 0000 4011 1c5f ac10 0a0a  E..F....@.._....
        0x0010:  ac10 0a32 8166 1618 0032 ea5f 5544 5020  ...2.f...2._UDP.
        0x0020:  7061 636b 6574 2066 726f 6d20 5b31 3732  packet.from.[172
        0x0030:  2e31 362e 3130 2e31 305d 2c5b 3536 3536  .16.10.10],[5656
        0x0040:  5d2c 5b33 5d0a                           ],[3].
20:50:47.303505 IP (tos 0x0, ttl 64, id 61932, offset 0, flags [none], proto UDP (17), length 70)
    203.0.113.10.39687 > 203.0.113.50.5656: [udp sum ok] UDP, length 42
        0x0000:  4500 0046 f1ec 0000 4011 1c5e ac10 0a0a  E..F....@..^....
        0x0010:  ac10 0a32 9b07 1618 0032 d0bd 5544 5020  ...2.....2..UDP.
        0x0020:  7061 636b 6574 2066 726f 6d20 5b31 3732  packet.from.[172
        0x0030:  2e31 362e 3130 2e31 305d 2c5b 3536 3536  .16.10.10],[5656
        0x0040:  5d2c 5b34 5d0a                           ],[4].
^C
3.3.7.2.2. Using Method 2

Using Method 2, first examine /etc/syslog.conf to see if there is already a facility and level for security listed. In modern versions of FreeBSD it is common to see:

security.*                      /var/log/security

ipfw creates logs with the LOG_SECURITY facility and sends output to the file /var/log/security in this case.

If this entry exists on your system, you are all set, otherwise read through the below and set up your own entry for an ipfw logfile.

To log to syslogd(8), add the following line to the end of /etc/syslog.conf:

security.info				/tmp/ipfw.info

(FreeBSD syslog.conf allows tabs or spaces to be used.)

Create the logfile with

# touch /tmp/ipfw.info

then send a HANGUP signal to the syslogd daemon:

# kill -HUP  <pid of syslogd>

You can use tail -f to see logs in real time.

Note that ipfw only logs matched rules with this method.

Note that ipfw logs to the .info level, and to the .debug level.

# tail -f /var/log/security
Apr  3 14:50:12 firewall newsyslog[401]: logfile first created
Apr  9 21:05:13 firewall kernel: ipfw: 1000 Accept UDP 203.0.113.10:59203 203.0.113.50:5656 in via em0
Apr  9 21:05:15 firewall kernel: ipfw: 1000 Accept UDP 203.0.113.10:12401 203.0.113.50:5656 in via em0
Apr  9 21:05:16 firewall kernel: ipfw: 1000 Accept UDP 203.0.113.10:45319 203.0.113.50:5656 in via em0
Apr  9 21:05:26 firewall kernel: ipfw: 1000 Accept UDP 203.0.113.10:26190 203.0.113.50:5656 in via em0
Apr  9 21:05:29 firewall kernel: ipfw: 1000 Accept UDP 203.0.113.10:55268 203.0.113.50:5656 in via em0
^C

The log entry includes the date, time, host, service, and rule number (1000 above) to make it easy to track which rule is being matched.

General Notes on logging

Consider the following ruleset:

# ipfw list
00100 check-state :default
01000 allow tcp from 203.0.113.100 to me setup keep-state :default
02000 allow icmp from 203.0.113.100 to me
02100 allow icmp from me to 203.0.113.100
03000 allow log udp from 203.0.113.10 to me 5656
04000 allow log logamount 20 udp from 203.0.113.10 to me 5657
05000 allow log logamount 20 udp from 203.0.113.10 to me 5658
65535 deny ip from any to any

Using Method 2 (syslog), on a quiet system, you may notice that the entries do not appear right away if you are reading the security log file in real time (for example, tail -f /var/log/security). This is because syslog will buffer identical lines and output a notification only occasionally as in the below example:

Mar 28 22:30:01 firewall kernel: ipfw: 3000 Accept UDP 203.0.113.10:27519 203.0.113.50:5656 in via em0
Mar 28 22:30:03 firewall syslogd: last message repeated 4 times
Mar 28 22:32:31 firewall syslogd: last message repeated 31 times

Also, Method 1 (using the ipfw0 interface) and Method 2 (syslog) are mutually exclusive. You cannot have both active at the same time. If net.inet.ip.fw.verbose=0, the output will be to the ipfw0 interface; if the value is 1, the log output will be to syslog.

logamount values in rules only apply to Method 2 - syslog. They have no effect on limiting the number of packets sent out the ipfw0 interface.

In Method 2 - syslog, when the log limit is reached, ipfw will send a notification similar to the following into the designated security logging file (default: /var/log/security):

Mar 28 23:00:10 firewall kernel: ipfw: 5000 Accept UDP 203.0.113.10:63367 203.0.113.50:5658 in via em0
Mar 28 23:00:11 firewall kernel: ipfw: 5000 Accept UDP 203.0.113.10:30909 203.0.113.50:5658 in via em0
Mar 28 23:00:11 firewall kernel: ipfw: limit 20 reached on entry 5000

And, at the same time, it also conveniently sends the same message to /var/log/messages, the standard FreeBSD logfile:

Mar 28 23:00:11 firewall kernel: ipfw: limit 20 reached on entry 5000

After that notification is sent, no more syslog entries will be sent from the matching rule until the log counters are reset with:

# ipfw resetlog <rule number>

When the resetlog command is entered, ipfw will send a reset notification to syslog:

Mar 28 23:04:51 firewall kernel: ipfw: logging count reset.

Unfortunately, as of FreeBSD version 14.1, it does not tell you which rule the count was reset for. Presumably, you should know which rule since you just entered the command, at least if you are the only person configuring the ruleset. In any case, you may have to keep track of that manually when working with many rules that include the log keyword.

If you issue an ipfw resetlog command without specifying a rule number, all counters in all rules are reset and ipfw sends the following notification:

Mar 28 23:08:52 firewall kernel: ipfw: All logging counts reset.

Finally, note that the sysctl variable net.inet.ip.fw.verbose_limit provides a "default limit" if one is not specified with the logamount keyword in the ruleset.

Consider this scenario:

# sysctl net.inet.ip.fw.verbose_limit
net.inet.ip.fw.verbose_limit: 5         <---- The limit is preset to 5

# ipfw list
65535 deny ip from any to any

As new rules are added, ipfw will apply any logamount value it finds in the body of a rule. If a rule being entered does not have a logamount entry, the value defaults to the current net.inet.ip.fw.verbose_limit amount.

# ipfw add 100 check-state
00100 check-state :default
#
# ipfw add 3000 allow log udp from 203.0.113.10 to me 5656
03000 allow log logamount 5 udp from 203.0.113.10 to me 5656
#
# ipfw add 4000 allow log logamount 20 udp from 203.0.113.10 to me 5657
04000 allow log logamount 20 udp from 203.0.113.10 to me 5657
#
# ipfw list
00100 check-state :default
03000 allow log logamount 5 udp from 203.0.113.10 to me 5656
04000 allow log logamount 20 udp from 203.0.113.10 to me 5657
65535 deny ip from any to any

If the sysctl for net.inet.ip.fw.verbose_limit is changed after the rule is entered, it has no effect:

# sysctl net.inet.ip.fw.verbose_limit=3
net.inet.ip.fw.verbose_limit: 5 -> 3

and later in /var/log/messages

Mar 29 11:07:02 firewall kernel: ipfw: limit 5 reached on entry 3000

 ...

Mar 29 11:07:24 firewall kernel: ipfw: limit 20 reached on entry 4000

3.3.8. Reset

The reset keyword sends an immediate TCP reset on a rule match containing that keyword. This immediately shuts down any TCP connection from the source matching the rule.

# ipfw list
00100 check-state :default
01000 allow log tcp from 203.0.113.10 to me 5656 setup keep-state :default
02000 reset log tcp from 203.0.113.10 to me 5657
03000 reset log udp from 203.0.113.10 to me 5658
65535 deny ip from any to any

The syslog view of a TCP reset rule match looks like this:

Apr  9 21:44:49 firewall kernel: ipfw: 1000 Accept TCP 203.0.113.50:5656 203.0.113.10:28218 out via em0
Apr  9 21:44:49 firewall syslogd: last message repeated 1 times
Apr  9 21:44:49 firewall kernel: ipfw: 1000 Accept TCP 203.0.113.10:28218 203.0.113.50:5656 in via em0
Apr  9 21:45:01 firewall kernel: ipfw: 2000 Reset TCP 203.0.113.10:12998 203.0.113.50:5657 in via em0
Apr  9 21:45:07 firewall kernel: ipfw: 2000 Reset TCP 203.0.113.10:13782 203.0.113.50:5657 in via em0

When sysctl net.inet.ip.fw.verbose=0, there is no discernable output on ipfw0 for a reset action. A TCP SYN packet arrives and that is all that is displayed. To actually witness the reset, run tcpdump(8) on the specific interface:

# tcpdump -i em0 -X -vvv
tcpdump: listening on em0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:53:39.376825 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    203.0.113.10.32945 > 203.0.113.50.5657: Flags [S], cksum 0x68fa (correct), seq 1926269947, win 65535, options [mss 1460,nop,wscale 6,sackOK,TS val 648984165 ecr 0], length 0
        0x0000:  4500 003c 0000 4000 4006 ce5f ac10 0a0a  E..<..@.@.._....
        0x0010:  ac10 0a32 80b1 1619 72d0 8bfb 0000 0000  ...2....r.......
        0x0020:  a002 ffff 68fa 0000 0204 05b4 0103 0306  ....h...........
        0x0030:  0402 080a 26ae b665 0000 0000            ....&..e....
21:53:39.377143 IP (tos 0x10, ttl 64, id 1965, offset 0, flags [none], proto TCP (6), length 40)
    203.0.113.50.5657 > 203.0.113.10.32945: Flags [R.], cksum 0xaddc (correct), seq 0, ack 1926269948, win 0, length 0
        0x0000:  4510 0028 07ad 0000 4006 06b7 ac10 0a32  E..(....@......2
        0x0010:  ac10 0a0a 1619 80b1 0000 0000 72d0 8bfc  ............r...
        0x0020:  5014 0000 addc 0000                      P.......
^C

A UDP rule containing the reset keyword just drops the packet. Nothing is sent back to the source address. If the log keyword is also used on the rule, a log entry is generated for syslog (if enabled):

Apr  9 21:58:49 firewall kernel: ipfw: 3000 Reset UDP 203.0.113.10:56503 203.0.113.50:5658 in via em0

3.3.9. Tee

The tee rule requires a divert(4) socket set up beforehand. Refer to the divert rule covered below for setting up the socket. Once the socket is set up, the tee keyword works like divert except that it is not interested in any packet return. It is simply copying the packet to the socket. Processing continues with the next rule.

In essence, tee allows the packet to be sent to userspace for any purpose desired - monitoring, copying, counting - whatever.

ipfw add 1000 tee 700 ip from any to me

3.3.10. Unreach

The unreach keyword directs ipfw to respond back to the source when packets arrive with a destination port that is not opened by any service. ipfw sends an ICMP reply with the code set to the keyword parameter. This works for any IP protocol.

Because ipfw sends an ICMP packet back to the source, the ruleset must allow outbound ICMP.

Consider the following ruleset:

# ipfw -a list
00100 0 0 allow icmp from me to any
01000 0 0 unreach 100 log udp from any to me 5656
02000 0 0 unreach 200 log tcp from any to me 5657
03000 0 0 unreach 250 log ip from any to me 5658

The counters are zero when the external1 VM sends its first packet, a UDP packet destined for port 5656.

ipfw matches this with rule 1000 and sends an ICMP unreachable notice with code 100 (arbitrary value, but see list in ipfw(8)). The offending packet is encapsulated in the data portion of the ICMP reply.

# tcpdump  -i bridge0 -X -vvv
tcpdump: listening on bridge0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:55:59.666002 IP (tos 0x0, ttl 64, id 25431, offset 0, flags [none], proto UDP (17), length 70)
    203.0.113.10.36146 > 203.0.113.50.5656: [udp sum ok] UDP, length 42
	0x0000:  4500 0046 6357 0000 4011 aaf3 ac10 0a0a  E..FcW..@.......
	0x0010:  ac10 0a32 8d32 1618 0032 de95 5544 5020  ...2.2...2..UDP.
	0x0020:  6174 7461 636b 2066 726f 6d20 5b31 3732  communication.from.[172
	0x0030:  2e31 362e 3130 2e31 305d 2c5b 3536 3536  .16.10.10],[5656
	0x0040:  5d2c 5b31 5d0a                           ],[1].

The results for a TCP unreachable are almost the same. The ICMP packet encapsulates the SYN packet in the data portion of the reply.

Here is a view of an ICMP Reply to unreachable TCP port 5657:

# tcpdump -i bridge0 -X -vvv
tcpdump: listening on bridge0, link-type EN10MB (Ethernet), capture size 262144 bytes
15:24:03.663104 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    203.0.113.10.58575 > 203.0.113.50.5657: Flags [S], cksum 0x092c (correct), seq 1062429515, win 65535, options [mss 1460,nop,wscale 6,sackOK,TS val 3566166113 ecr 0], length 0
	0x0000:  4500 003c 0000 4000 4006 ce5f ac10 0a0a  E..<..@.@..-....
	0x0010:  ac10 0a32 e4cf 1619 3f53 634b 0000 0000  ...2....?ScK....
	0x0020:  a002 ffff 092c 0000 0204 05b4 0103 0306  .....,..........
	0x0030:  0402 080a d48f 6061 0000 0000            ......`a....
15:24:03.664168 IP (tos 0x0, ttl 64, id 37717, offset 0, flags [none], proto ICMP (1), length 88)
    203.0.113.50 > 203.0.113.10: ICMP # 200 203.0.113.50 unreachable, length 68
	IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    203.0.113.10.58575 > 203.0.113.50.5657: Flags [S], cksum 0x092c (correct), seq 1062429515, win 65535, options [mss 1460,nop,wscale 6,sackOK,TS val 3566166113 ecr 0], length 0
	0x0000:  4500 0058 9355 0000 4001 7af3 ac10 0a32  E..X.U..@.z....2
	0x0010:  ac10 0a0a 03c8 68c3 0000 0000 4500 003c  ......h.....E..<
	0x0020:  0000 4000 4006 ce5f ac10 0a0a ac10 0a32  ..@.@.._.......2
	0x0030:  e4cf 1619 3f53 634b 0000 0000 a002 ffff  ....?ScK........
	0x0040:  092c 0000 0204 05b4 0103 0306 0402 080a  .,..............
	0x0050:  d48f 6061 0000 0000                      ..`a....

3.3.11. Setdscp

The setdcsp action directs ipfw to set an IP header option on outbound packets. The action has no effect on inbound packets. The header option, formerly known as the "Type of Service" (ToS) option, now defines several classes of differentiated services (DiffServ) per several RFCs - RFC 2474, RFC 3168, and RFC 3260.

These service classes such as "Network Control", "Telephony", "Multimedia Conferencing", "Broadcast Video", "Low-latency Data", etc. all require their packets receive special handling in the network. The handling is achieved by inserting "code points" - numerical values in the packet header - that define each class.

Firewalls, routers, switches, and other network devices interpret these values and, in theory, service the packets according to their class. See this Wikipedia article on Differentiated Services: https://en.wikipedia.org/wiki/Differentiated_services.

In practice, support for service classes vary among network operators. Check the man page for a list of code points settable by ipfw.

The example below sets the DSCP value to "af31", a codepoint in the "Multimedia Streaming" class.

# ipfw add 2000 setdscp af31 udp from me to any 5656

Note that rule processing continues to the next rule.

Note also that the DSCP value takes up only a partial byte in the IP header, sharing it with two bits of ECN (Explicit Congestion Notification). The binary value for "af31" is 011010nn, where 'nn' are the two bits for ECN. If no ECN, the value resolves to 0x68 (104 decimal).

An outbound traffic example, generated by ncat -u 203.0.113.10 5656 from the firewall VM is shown as received by external1 VM:

# tcpdump -i em0 -X -vvv
tcpdump: listening on em0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:14:22.173252 IP (tos 0x68, ttl 64, id 30816, offset 0, flags [none], proto UDP (17), length 57)
    203.0.113.50.17767 > 203.0.113.10.5656: [udp sum ok] UDP, length 29
        0x0000:  4568 0039 7860 0000 4011 958f ac10 0a32  Eh.9x`..@......2
        0x0010:  ac10 0a0a 4567 1618 0025 0f5b 4772 6565  ....Eg...%.[Gree
        0x0020:  7469 6e67 7320 6672 6f6d 2074 6865 2066  tings.from.the.f
        0x0030:  6972 6577 616c 6c2e 0a                   irewall..

Diffserve codepoints can be set on any IP based protocol or restricted to selected protocols and/or ports through suitable rules.

3.3.12. Skipto

The skipto action directs the firewall engine to pass over any rules less than the skipto parameter number. If an early rule can match a packet characteristic such as an address, port, TCP or UDP header option or similar, a skipto rule can jump to a potentially much later section of the firewall ruleset to handle to packet.

Consider the following (contrived) ruleset:

# ipfw add 100 check-state
# ipfw add 1000 allow tcp from me to any established keep-state
# ipfw add 2000 allow tcp from 203.0.113.10 to me 4500 setup keep-state
# ipfw add 3000 allow tcp from 203.0.113.10 to me 4502 setup keep-state
# ipfw add 4000 allow tcp from 203.0.113.10 to me 4504 setup keep-state
# ipfw add 5000 allow tcp from 203.0.113.10 to me 4506 setup keep-state
# ipfw add 6000 allow tcp from 203.0.113.10 to me 4508 setup keep-state
# ipfw add 7000 allow tcp from 203.0.113.10 to me 4510 setup keep-state
# ipfw add 8000 allow tcp from 203.0.113.10 to me 4512 setup keep-state
# ipfw add 9000 allow tcp from 203.0.113.10 to me 4512 setup keep-state
# ipfw add 10000 allow tcp from 203.0.113.10 to me 5656 setup keep-state

With the external1 VM using our standard "tcon.sh 5656" TCP connection script, the firewall VM has to traverse the entire firewall ruleset, checking each rule in turn for a match. When testing this ruleset, ensure that the firewall VM is running the appropriate service script such as tserv3.sh.

By placing a skipto action rule after the check-state action, we can have ipfw jump directly to the rule we care about:

# ipfw add 100 check-state
# *ipfw add 500 skipto 10000 tcp from 203.0.113.10 to me 5656
# ipfw add 1000 allow tcp from me to any established keep-state
# ipfw add 2000 allow tcp from 203.0.113.10 to me 4500 setup keep-state
# ipfw add 3000 allow tcp from 203.0.113.10 to me 4502 setup keep-state
# ipfw add 4000 allow tcp from 203.0.113.10 to me 4504 setup keep-state
# ipfw add 5000 allow tcp from 203.0.113.10 to me 4506 setup keep-state
# ipfw add 6000 allow tcp from 203.0.113.10 to me 4508 setup keep-state
# ipfw add 7000 allow tcp from 203.0.113.10 to me 4510 setup keep-state
# ipfw add 8000 allow tcp from 203.0.113.10 to me 4512 setup keep-state
# ipfw add 9000 allow tcp from 203.0.113.10 to me 4512 setup keep-state
# ipfw add 10000 allow tcp from 203.0.113.10 to me 5656 setup keep-state

You can determine if your skipto action is working by using the -a command line parameter:

# ipfw -a list
00100 0   0 check-state :default
00500 1  60 skipto 10000 tcp from 203.0.113.10 to me 5656
01000 0   0 allow tcp from me to any established keep-state :default
02000 0   0 allow tcp from 203.0.113.10 to me 4500 setup keep-state :default
03000 0   0 allow tcp from 203.0.113.10 to me 4502 setup keep-state :default
04000 0   0 allow tcp from 203.0.113.10 to me 4504 setup keep-state :default
05000 0   0 allow tcp from 203.0.113.10 to me 4506 setup keep-state :default
06000 0   0 allow tcp from 203.0.113.10 to me 4508 setup keep-state :default
07000 0   0 allow tcp from 203.0.113.10 to me 4510 setup keep-state :default
08000 0   0 allow tcp from 203.0.113.10 to me 4512 setup keep-state :default
09000 0   0 allow tcp from 203.0.113.10 to me 4514 setup keep-state :default
10000 8 474 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
65535 0   0 deny ip from any to any

General notes on skipto:

  • The action does not allow negative numbers or zero as a parameter.

  • skipto does allow numbers greater than 65536. What happens is the system accepts the input, but the result is the integer remainder of the number modulo 65536. You should ensure the value you enter is the value displayed by the command output. Numbers greater than 65534 should not be used.

  • You can use the skipto action to skip between sets. However, if the set containing the skipto target is disabled, processing continues with the next rule in any set that is enabled.

For example, if you have three sets - 0, 1, and 2, with a disabled set 1 containing the destination of the skipto action, processing will continue with the next rule. See the below ruleset and counters:

# ipfw -Sa list
00100 0   0 set 0 check-state :default
00101 1  60 set 0 skipto 2000 tcp from 203.0.113.10 to me
00120 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00150 0   0 set 0 allow tcp from me to any established keep-state :default
01000 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
# DISABLED 01200 0   0 set 1 allow tcp from 203.0.113.10 to me 6500 setup keep-state :default
# DISABLED 01800 0   0 set 1 allow tcp from 203.0.113.10 to me 6512 setup keep-state :default
# DISABLED 01900 0   0 set 1 allow tcp from 203.0.113.10 to me 6514 setup keep-state :default
# DISABLED 02000 0   0 set 1 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
02700 0   0 set 2 allow tcp from 203.0.113.10 to me 7510 setup keep-state :default
02800 0   0 set 2 allow tcp from 203.0.113.10 to me 7512 setup keep-state :default
02900 0   0 set 2 allow tcp from 203.0.113.10 to me 7514 setup keep-state :default
03000 8 474 set 2 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
65535 0   0 set 31 deny ip from any to any
  • If you skipto a rule number that has multiple rules, the first matching rule at or after that number is executed:

# ipfw -Sa list
00100  0   0 set 0 check-state :default
00101  1  60 set 0 skipto 2000 tcp from 203.0.113.10 to me
00120  0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00150  0   0 set 0 allow tcp from me to any established keep-state :default
01600  0   0 set 1 allow tcp from 203.0.113.10 to me 6508 setup keep-state :default
01700  0   0 set 1 allow tcp from 203.0.113.10 to me 6510 setup keep-state :default
02000  0   0 set 1 allow tcp from 203.0.113.10 to me 6512 setup keep-state :default
02000  0   0 set 1 allow tcp from 203.0.113.10 to me 6514 setup keep-state :default
02000  0   0 set 2 allow tcp from 203.0.113.10 to me 7500 setup keep-state :default
02000  0   0 set 2 allow tcp from 203.0.113.10 to me 7502 setup keep-state :default
02000  0   0 set 2 allow tcp from 203.0.113.10 to me 7504 setup keep-state :default
02500  0   0 set 2 allow tcp from 203.0.113.10 to me 7506 setup keep-state :default
02600  0   0 set 2 allow tcp from 203.0.113.10 to me 7508 setup keep-state :default
02700  0   0 set 2 allow tcp from 203.0.113.10 to me 7510 setup keep-state :default
02800  0   0 set 2 allow tcp from 203.0.113.10 to me 7512 setup keep-state :default
02900  0   0 set 2 allow tcp from 203.0.113.10 to me 7514 setup keep-state :default
03000 10 567 set 2 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
65535  0   0 set 31 deny ip from any to any
  • You can enter a rule with a skipto rule number that is lower than the current rule number, attempting to go backward in the ruleset, but this has no effect, and processing continues with the next rule:

# ipfw -Sa list
00100 0   0 set 0 check-state :default
00101 1  60 set 0 skipto 1000 tcp from 203.0.113.10 to me
00120 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00150 0   0 set 0 allow tcp from me to any established keep-state :default
00200 0   0 set 0 allow tcp from 203.0.113.10 to me 4500 setup keep-state :default
00300 0   0 set 0 allow tcp from 203.0.113.10 to me 4502 setup keep-state :default
00400 0   0 set 0 allow tcp from 203.0.113.10 to me 4504 setup keep-state :default
00500 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00600 0   0 set 0 allow tcp from 203.0.113.10 to me 4508 setup keep-state :default
00700 0   0 set 0 allow tcp from 203.0.113.10 to me 4512 setup keep-state :default
00800 0   0 set 0 allow tcp from 203.0.113.10 to me 4514 setup keep-state :default
01000 1  60 set 0 skipto 500 tcp from 203.0.113.10 to me
01100 8 475 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
65535 0   0 set 31 deny ip from any to any
  • You can (but probably should not) skipto a skipto rule:

00100 0   0 set 0 check-state :default
00101 1  60 set 0 skipto 1000 tcp from 203.0.113.10 to me
00120 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00150 0   0 set 0 allow tcp from me to any established keep-state :default
00200 0   0 set 0 allow tcp from 203.0.113.10 to me 4500 setup keep-state :default
00300 0   0 set 0 allow tcp from 203.0.113.10 to me 4502 setup keep-state :default
00400 0   0 set 0 allow tcp from 203.0.113.10 to me 4504 setup keep-state :default
00500 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
00600 0   0 set 0 allow tcp from 203.0.113.10 to me 4508 setup keep-state :default
00700 0   0 set 0 allow tcp from 203.0.113.10 to me 4510 setup keep-state :default
00800 0   0 set 0 allow tcp from 203.0.113.10 to me 4512 setup keep-state :default
00900 0   0 set 0 allow tcp from 203.0.113.10 to me 4514 setup keep-state :default
01000 1  60 set 0 skipto 1500 tcp from 203.0.113.10 to me
01000 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
01500 1  60 set 0 skipto 2000 tcp from 203.0.113.10 to me
01600 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
02000 1  60 set 0 skipto 2500 tcp from 203.0.113.10 to me
02100 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
02500 1  60 set 0 skipto 3000 tcp from 203.0.113.10 to me
02600 0   0 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
03000 8 475 set 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
65535 0   0 set 31 deny ip from any to any

See the section on Lists for additional caveats.

3.3.13. Divert

The divert mechanism in ipfw allows you to pull packets into user space for programmatic purposes. The divert rule snatches the packet and presents it to a divert(4) socket, a special socket type that can be created from an external program. See Appendix B for the sample program used for this book.

Divert sockets can be used as the basis for many specialized applications such as packet examination, in-flight packet modification, experimental routing techniques, etc. The program shown here simply reads from the socket and dumps the contents of the packet in hex and ASCII. It then writes the packet back into the divert socket.

To work with the divert keyword, the divert(4) packet diversion mechanism has to be compiled into the kernel or loaded at runtime:

# kldload ipdivert

This loads the divert kernel module which provides divert(4) functionality.

Once this is done, an application can open a divert(4) socket and process packets.

argv[1] = 700
after strtonum.  num = [700]
Opening divert on port 700

...  (see below)

In another window, run netstat(1) to see the divert socket:

# netstat -an | more
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address          Foreign Address        (state)
tcp4       0      0 *.22                   *.*                    LISTEN
tcp6       0      0 *.22                   *.*                    LISTEN
udp4       0      0 *.514                  *.*
udp6       0      0 *.514                  *.*
div4       0      0 *.700                  *.*
Active UNIX domain sockets
Address          Type   Recv-Q Send-Q            Inode             Conn             Refs     Nextref Addr
fffff80003b83000 stream      0      0 fffff80003cac5a0                0                0                0 /var/run/devd.pipe
fffff80003baf800 dgram       0      0                0 fffff80003bafc00                0 fff
...

To examine the divert operation, we first create a suitable rule:

# ipfw add 700 divert 700 ip from any to any

The syntax is a bit odd in this case. The divert keyword takes a number argument that functions as the divert object. This is similar syntax to pipes, queues, and NAT (network address translation) rules which we will see later.

A common convention, though not required, is to make the divert port the same number as the rule number in the ruleset. Whatever the rule number, when the packet is diverted and processed, and then returned to ipfw, the firewall picks up the packet at the divert rule number, plus one - that is, the next rule.

# ipfw list
00700 divert 700 ip from any to any
01000 allow udp from 203.0.113.10 to me
01100 allow udp from me to 203.0.113.10
65535 deny ip from any to any

Let’s look at the ruleset above. The packet is diverted to a divert socket, port 700 at the first rule. When it is returned from the mydivert program, it renters the ruleset at rule 1000. The ruleset allows UDP packets from and to the external1 VM.

If we set up the firewall VM host to run userv3.sh and the external1 VM host to run ucon.sh 5656, we get the following expected output from the divert program:

# ./divert 700
Opening divert on port 700
203.0.113.10:51417 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 0c 00 00 40 11 65 3e ac 10 0a 0a  |E..F....@.e>....|
|0010   ac 10 0a 32 c8 d9 16 18 00 32 a2 eb 55 44 50 20  |...2.....2..UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 34 5d 0a                                |],[4].          |

And we also see the output from userv3.sh on the firewall console:

# sh userv3.sh
Starting UDP listeners on [5656],[5657],[5658]
UDP communication from [203.0.113.10],[5656],[1]

However, if we shut down the userv3.sh services on the firewall VM, the incoming packets find no open port and are rejected by the firewall VM host. However, they still go through the divert socket:

# ./mydivert 700
argc = 2
argv[1] = 700
after strtonum.  num = [700]
Opening divert on port 700
203.0.113.10:26058 -> 203.0.113.50:5656
|0000   45 00 00 47 a9 12 00 00 40 11 65 37 ac 10 0a 0a  |E..G....@.e7....|
|0010   ac 10 0a 32 65 ca 16 18 00 33 28 a9 55 44 50 20  |...2e....3(.UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 31 30 5d 0a                             |],[10].         |
203.0.113.50:771 -> 203.0.113.10:27038
|0000   45 00 00 63 3a 61 00 00 40 01 65 37 ac 10 0a 32  |E..c:a..@.e7...2|
|0010   ac 10 0a 0a 03 03 69 9e 00 00 00 00 45 00 00 47  |......i.....E..G|
|0020   a9 12 00 00 40 11 65 37 ac 10 0a 0a ac 10 0a 32  |....@.e7.......2|
|0030   65 ca 16 18 00 33 28 a9 55 44 50 20 70 61 63 6b  |e....3(.UDP pack|
|0040   65 74 20 66 72 6f 6d 20 5b 31 37 32 2e 31 36 2e  |et from [172.16.|
|0050   31 30 2e 31 30 5d 2c 5b 35 36 35 36 5d 2c 5b 31  |10.10],[5656],[1|
|0060   30 5d 0a                                         |0].             |
mydivert: sendto: Permission denied

The last output line, "Permission denied", is because the kernel, faced with a packet and no port to send it to, instead sends an ICMP port unreachable response. The kernel sends the ICMP packet back out the divert port, but there is no ipfw rule for it to re-enter the firewall - thus "Permission denied". The packet is dropped.

To fix, we add a rule for ICMP traffic in either direction:

# ipfw list
00700 divert 700 ip from any to any
00800 allow icmp from any to any
01000 allow udp from 203.0.113.10 to me
01100 allow udp from me to 203.0.113.10
65535 deny ip from any to any

The divert operation now works as expected and the packet re-enters the firewall after rule 700. The next rule (800) permits ICMP in either direction and the packet is sent back to the source host. In the listings below, the ucon.sh script was run for 5 cycles, and after cycle #3, the firewall userv3.sh script was shut down.

The remaining two cycles result in an ICMP message being returned back to the external1 VM:

# ./mydivert 700
Opening divert on port 700
203.0.113.10:36083 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 20 00 00 40 11 65 2a ac 10 0a 0a  |E..F. ..@.e*....|
|0010   ac 10 0a 32 8c f3 16 18 00 32 de d4 55 44 50 20  |...2.....2..UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 31 5d 0a                                |],[1].          |
203.0.113.10:25662 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 21 00 00 40 11 65 29 ac 10 0a 0a  |E..F.!..@.e)....|
|0010   ac 10 0a 32 64 3e 16 18 00 32 07 89 55 44 50 20  |...2d>...2..UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 32 5d 0a                                |],[2].          |
203.0.113.10:40345 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 22 00 00 40 11 65 28 ac 10 0a 0a  |E..F."..@.e(....|
|0010   ac 10 0a 32 9d 99 16 18 00 32 ce 2c 55 44 50 20  |...2.....2.,UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 33 5d 0a                                |],[3].          |
203.0.113.10:53482 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 23 00 00 40 11 65 27 ac 10 0a 0a  |E..F.x..@.e'....|
|0010   ac 10 0a 32 d0 ea 16 18 00 32 9a da 55 44 50 20  |...2.....2..UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 34 5d 0a                                |],[4].          |
203.0.113.50:771 -> 203.0.113.10:27037   (ICMP packet)
|0000   45 00 00 62 3a 68 00 00 40 01 65 27 ac 10 0a 32  |E..b:h..@.e'...2|
|0010   ac 10 0a 0a 03 03 69 9d 00 00 00 00 45 00 00 46  |......i.....E..F|
|0020   a9 23 00 00 40 11 65 27 ac 10 0a 0a ac 10 0a 32  |.x..@.e'.......2|
|0030   d0 ea 16 18 00 32 9a da 55 44 50 20 70 61 63 6b  |.....2..UDP pack|
|0040   65 74 20 66 72 6f 6d 20 5b 31 37 32 2e 31 36 2e  |et from [172.16.|
|0050   31 30 2e 31 30 5d 2c 5b 35 36 35 36 5d 2c 5b 34  |10.10],[5656],[4|
|0060   5d 0a                                            |].              |
203.0.113.10:35359 -> 203.0.113.50:5656
|0000   45 00 00 46 a9 24 00 00 40 11 65 26 ac 10 0a 0a  |E..F.$..@.e&....|
|0010   ac 10 0a 32 8a 1f 16 18 00 32 e1 a4 55 44 50 20  |...2.....2..UDP |
|0020   70 61 63 6b 65 74 20 66 72 6f 6d 20 5b 31 37 32  |packet from [172|
|0030   2e 31 36 2e 31 30 2e 31 30 5d 2c 5b 35 36 35 36  |.16.10.10],[5656|
|0040   5d 2c 5b 35 5d 0a                                |],[5].          |
203.0.113.50:771 -> 203.0.113.10:27037   (ICMP packet)
|0000   45 00 00 62 3a 69 00 00 40 01 65 26 ac 10 0a 32  |E..b:i..@.e&...2|
|0010   ac 10 0a 0a 03 03 69 9d 00 00 00 00 45 00 00 46  |......i.....E..F|
|0020   a9 24 00 00 40 11 65 26 ac 10 0a 0a ac 10 0a 32  |.$..@.e&.......2|
|0030   8a 1f 16 18 00 32 e1 a4 55 44 50 20 70 61 63 6b  |.....2..UDP pack|
|0040   65 74 20 66 72 6f 6d 20 5b 31 37 32 2e 31 36 2e  |et from [172.16.|
|0050   31 30 2e 31 30 5d 2c 5b 35 36 35 36 5d 2c 5b 35  |10.10],[5656],[5|
|0060   5d 0a                                            |].              |
^C

Note the two icmp packets logged by rule 800:

# ipfw show
00700  19   2466 divert 700 ip from any to any
00800   2    196 allow icmp from any to any
01000   5    350 allow udp from 203.0.113.10 to me
01100   0      0 allow udp from me to 203.0.113.10
65535 477 114991 deny ip from any to any

General notes on the divert action:

  • The ipdivert.ko kernel module must be loaded or compiled into the kernel to create a divert rule, and thus to use divert(4) sockets.

  • You cannot create a rule with a divert port of 0 or 65535. The port number must be between 1 and 65534 (inclusive). *If you create a rule with a divert port on rule 65534, the returning packet will restart firewall rule processing at the default rule, 65535, which cannot be changed.

  • You can create a divert rule for any protocol in /etc/protocols.

  • You can use the same divert port for multiple rules.

  • After returning from a divert rule, if the next rule is in another set, processing will continue with that rule unless the set is disabled. If disabled it will skip to the next rule in any set that is not disabled.

General notes on creating the divert socket:

  • Only root can create a divert socket.

  • Opening a divert socket on port 0 or port 65536 results in a random divert port number.

  • Opening a divert socket on port 65535 is permitted, but not advised.

  • Opening a divert port greater than 65536 or less than 0 results in a positive port number modulo 65536.

  • As with other sockets, you cannot open two divert sockets on the same port number. However, you can open a divert socket on a port already in use for any protocols based on IPv4 or IPv6.

3.3.14. Other Protocols

You can use any protocol in /etc/protocols in a rule.

# ipfw add 1000 allow ospf from any to me
# ipfw add 2000 allow chaos from any to me
etc.

3.3.15. Limit

ipfw can restrict the number of active connections with the limit option. This option allows for specifying a parameter in the rule that is regarded as a flow element, e.g. one of src-addr, src-port, dst-addr, or dst-port. In addition, the limit keyword takes a value, N, that is the maximum number of connections desired.

Concurrent connections via TCP, UDP, ICMP, or any protocol can be limited in this way.

ipfw creates a dynamic rule for each connection allowed by the rule. When the maximum number of connections is reached, additional packets are considered no longer matched and are dropped by the rule after being counted, and the search terminates.

To test, we write a simple script on the external1 VM to write UDP packets multiple times to the firewall VM:

# export NUM=1
# for i in 5656 5657 5658 5656 5657 5658 5656 5657 5658 5656
> do
>   echo "hello [$NUM] to port [$i]" | ncat -u 203.0.113.50 $i
>   NUM=expr $NUM + 1
> done

On the firewall VM, ipfw starts creating dynamic rules as soon as the first matching packet is received. Additional dynamic rules, up to the limit number are created. Each UDP based dynamic rule has a default 10 second lifetime, controlled by the sysctl node net.inet.ip.fw.dyn_udp_lifetime. As they expire under the limit value, space for additional connections is created. The number of open dynamic rules at any point in time can be viewed with the sysctl node net.inet.ip.fw.dyn_count.

# ipfw -SaD list
00500  0   0 check-state :default
01000 10 531 allow udp from 203.0.113.10 to me limit src-addr 3 :default
65535  0   0 deny ip from any to any
## Dynamic rules (4 560):
01000  1  53 (8s) LIMIT udp 203.0.113.10 23755 <-> 203.0.113.50 5657 :default
01000  1  53 (8s) LIMIT udp 203.0.113.10 30144 <-> 203.0.113.50 5656 :default
01000  0   0 (4s) PARENT 3 udp 203.0.113.10 0 <-> 0.0.0.0 0 :default
01000  1  53 (8s) LIMIT udp 203.0.113.10 22722 <-> 203.0.113.50 5658 :default
# sh userv3.sh
Starting UDP listeners on [5656],[5657],[5658]
hello [1] to port [5656]
hello [2] to port [5657]
hello [3] to port [5658]
^C

3.3.16. Call and Return

The call and return actions allow you to change ruleset processing by jumping to a rule number elsewhere in the ruleset. If the rules at that location contain a return action, processing will jump back to the statement immediately after the original call statement. In practice, this acts like a program function call, or as the man page notes, like an assembly language subroutine.

Creating a ruleset with call and return actions:

#
# ipfw add 500 check-state
00500 check-state :default
#
# ipfw add 1000 call 20000 udp from 203.0.113.10 to me 5656
01000 call 20000 udp from 203.0.113.10 to me 5656
#
# ipfw add 1100 count udp from 203.0.113.10 to me
01100 count udp from 203.0.113.10 to me
#
# ipfw add 1200 allow udp from 172.168.10.10 to me 5656
01200 allow udp from 203.0.113.10 to me 5656
#
# ipfw add 20000 count udp from 203.0.113.10 to me
20000 count udp from 203.0.113.10 to me
#
# ipfw add 21000 return via any
21000 return
#
# ipfw -a list
00500 0 0 check-state :default
01000 0 0 call 20000 udp from 203.0.113.10 to me 5656
01100 0 0 count udp from 203.0.113.10 to me
01200 0 0 allow udp from 203.0.113.10 to me 5656
20000 0 0 count udp from 203.0.113.10 to me
21000 0 0 return
65535 0 0 deny ip from any to any

As noted in the man page, you have to put some extra syntactic sugar on the return statement:

# ipfw add 21000 return via any

In this example we have used several count statements to try to trace ruleset processing.

To test, we have the firewall VM host startup 3 listeners with userv3.sh. After the external1 VM uses ucon.sh 5656 to send a udp packet, the count list looks like this:

# ipfw -a list
00500 0  0 check-state :default
01000 1 70 call 20000 udp from 203.0.113.10 to me 5656
01100 1 70 count udp from 203.0.113.10 to me
01200 1 70 allow udp from 203.0.113.10 to me 5656
20000 1 70 count udp from 203.0.113.10 to me
21000 1 70 return
65535 0  0 deny ip from any to any

The packet was matched at rule 1000 where it encountered a call action to jump to rule 20000 where it was then matched at 20000. The next rule was a matched return action at rule 21000. Returning to the rule after the call action, it matched a count action at 1100, then matched an allow action at 1200 where it was sent through to the application layer and was received by userv3.sh.

If we remove the return action at rule 21000, the counts look much different:

# ipfw -a list
00500 0  0 check-state :default
01000 1 70 call 20000 udp from 203.0.113.10 to me 5656
01100 0  0 count udp from 203.0.113.10 to me
01200 0  0 allow udp from 203.0.113.10 to me 5656
20000 1 70 count udp from 203.0.113.10 to me
65535 1 70 deny ip from any to any

Without a return at rule 21000, the only rule left is the default deny rule, and there is nothing received by userv3.sh.

Because you can jump both forward and backward with the call action, it is possible to create an endless loop.

In this example we create an endless loop with in incorrect call rule:

#
# ipfw -a list
01000 0 0 count udp from 203.0.113.10 to me
05000 0 0 check-state :default
06000 0 0 call 1000 udp from 203.0.113.10 to me 5656
07000 0 0 count udp from 203.0.113.10 to me
65535 0 0 deny ip from any to any

In this example we are missing a return action between rule 1000, the target of the call action at rule 6000. This creates a loop. ipfw eventually figures out that a loop exists, and breaks out at the next call action with the diagnostic:

# ipfw: call stack error, go to next rule

but it does not currently tell you where the missing return action is or which rule it went to next.

You can pick up a clue by watching the rule counts. Below is the rule count for this errant ruleset after just one packet was received from the external1 VM:

# ipfw -a list
01000 17 1207 count udp from 203.0.113.10 to me
05000  0    0 check-state :default
06000 16 1136 call 1000 udp from 203.0.113.10 to me 5656
07000  1   71 count udp from 203.0.113.10 to me
65535  1   71 deny ip from any to any

This shows that ipfw went around this call loop 16 times before throwing an error.

For TCP connections, call and return operate almost the same. Below is a ruleset with TCP instead of UDP for the desired protocol and including the required setup and keep-state keywords:

# ipfw -a list
00500 0 0 check-state :default
01000 0 0 call 20000 tcp from 203.0.113.10 to me 5656
01100 0 0 count tcp from 203.0.113.10 to me
02000 0 0 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
20000 0 0 count tcp from 203.0.113.10 to me
21000 0 0 return
65535 0 0 deny ip from any to any

After the connection is successfully made from external1, the observed counts are:

# ipfw -a list
00500 0   0 check-state :default
01000 1  60 call 20000 tcp from 203.0.113.10 to me 5656
01100 1  60 count tcp from 203.0.113.10 to me
02000 8 479 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
20000 1  60 count tcp from 203.0.113.10 to me
21000 1  60 return
65535 0   0 deny ip from any to any

The difference in rule counts is due to dynamic rules created by the setup and keep-state keywords on rule 2000. If we manually run the connection we can observe the counts with the -d command line parameter. The numbers below are from just the initial 3-way handshake:

# ipfw -ad list
00500 0   0 check-state :default
01000 1  60 call 20000 tcp from 203.0.113.10 to me 5656
01100 1  60 count tcp from 203.0.113.10 to me
02000 5 276 allow tcp from 203.0.113.10 to me 5656 setup keep-state :default
20000 1  60 count tcp from 203.0.113.10 to me
21000 1  60 return
65535 0   0 deny ip from any to any
## Dynamic rules (1 152):
02000 5 276 (296s) STATE tcp 203.0.113.10 19179 <-> 203.0.113.50 5656 :default

The above numbers indicate that the 3-way handshake occurred during the dynamic rule setup.

General notes on call and return:

  • You can call out to an address either before or after the current rule.

  • You must have a return for every call.

  • You can nest call / return pairs up to 16 levels deep. If you add another call rule, ipfw will throw the "call stack error, go to next rule" error and continue with the next rule.

  • Similar to skip-to, you cannot call to rule 0, 65535, or 65536. If you call to a value greater then 65536, the rule is accepted and the call target rule number is the requested value modulo 65536.

  • Similar also to sets, if a call is made to a target rule in a set that is disabled, the call will land on the next non-disabled rule in any set and processing continues from there.

  • If a return is encountered when no call has been made, the return rule is ignored and processing continues with the next rule.

3.3.17. Using uid and gid in rules

An interesting capability of ipfw is its ability to match packets on Unix uid and gid values. Network packets themselves have no inherent ownership, so where does this capability come from? Answer - it comes from the applications that are the source and destination of those packets.

Let’s look at a simple example. First, let’s examine the actual syntax that allows this matching capability.

Locate or add a user (here 'quarven') if needed:

# grep quarven /etc/passwd
quarven:*:1002:1002:Quarven:/home/quarven:/bin/sh

Now use 'quarven' in an ipfw rule:

# ipfw add 700 allow udp from 203.0.113.10 to me uid quarven
00700 allow udp from 203.0.113.10 to me uid quarven
#
# ipfw show
00700 0 0 allow udp from 203.0.113.10 to me uid quarven
65535 0 0 deny ip from any to any

Next, login as user 'quarven' and run the script /root/bin/userv.sh 5656. Then switch to the external1 VM and run /root/bin/ucon.sh 5656 for just one cycle.

The results are shown below.

quarven@firewall:~ $ /bin/sh /root/bin/userv.sh 5656
PORT1 = [5656]
Starting UDP listener on [203.0.113.50],[5656]
UDP packet from [203.0.113.10],[5656],[1]
^Cquarven@firewall:~ $

This works because the instance of userv.sh is run under the uid quarven as shown below

Show the user information for the userv.sh instance:

USER      PID  PPID  PGID   SID JOBC STAT TT       TIME COMMAND
quarven 51689 27186 51689 27186    1 I    v0    0:00.08 -sh (sh)
quarven 54015 51689 54015 27186    1 I+   v0    0:00.02 /bin/sh /root/bin/userv.sh 5656
quarven 56230 54015 54015 27186    1 I+   v0    0:00.02 nc -l -k -u 203.0.113.50 5656

ipfw has matched an incoming network packet to a program owned by a userid. If the rule is changed to another userid, even root, the match will not succeed and the packet will be picked up by the default rule.

It is sometimes necessary to shut down all traffic from a certain user. This capability is ideal for that purpose:

# ipfw add 200 deny ip from any to any uid quarven

Notes on using uid and gid:

  • The gid keyword works in an identical fashion to uid described above.

  • If using a name as the uid or gid, the name must exist in the indicated system file.

  • Ranges and lists are not allowed. You can’t use "…​ uid tom,dick,harry" or "…​ uid 1000-1002".

  • Outbound traffic works in a similar way, just reversing the source and destination.

  • Denied traffic will generally have an indication of "<application>: sendto: Permission denied"

  • ICMP traffic cannot be reliably filtered using uid/gid. This is a known limitation.

  • As noted in ipfw(8) some contexts, such as initial incoming SYN packets may have no uid/gid associated with them.

  • Programs using setuid(2) system calls may not behave as expected, though it may be possible to set the uid/gid to the effective id if it can be determined.

  • Using uid/gid keywords for matching is resource intensive and should be used sparingly if at all.

3.4. Lookup Tables

Lookup tables are a versatile feature of many firewall systems, including ipfw. A lookup table is a virtual container that holds tuples of elements, one of which is a key that functions as a fast lookup feature. Using a key provided by a rule, ipfw can quickly determine if the element is in the table. If it is, that portion of the rule is matched.

Lookup tables are a powerful feature of ipfw and useful in many situations.

ipfw provides four types of lookup tables:

  • Address tables (addr) - These tables hold addresses that ipfw can rapidly find with an address as a key. If the address is matched the lookup is considered matched and used by the associated rule. This table type takes an additional keyword 'valtype' that can be used to specify IPv4 addresses or IPv6 addresses.

  • Interface tables (iface) - These tables hold interface names. Each entry is the name of an interface. Note that wildcards such as em* are not supported.

  • Number tables (number) - These tables are used for protocols, ports, uids/gids, or jail ids. Entries are 32 bit unsigned integers. Ranges (e.g. 1234-5678) are not supported.

  • Flow tables (flow) - These tables contain flow type suboptions that are used in looking up existing traffic flows.

3.4.1. Creating Lookup Tables

All tables must be created before they can be referenced by a rule. Note that the commands to manage tables do not have line numbers - they are independent shell commands that exist outside the ruleset.

# ipfw table foo create

By default, the ipfw table create command creates tables of type addr. Table names share the same namespace and so must be unique even among tables of different types.

To specify other types, add the type keyword:

# ipfw table bar create type iface
#
# ipfw table bar create type flow
#
# ipfw table bop create type number

Previously, when creating a table of type number, a bug existed that required an algorithm option such as number:array. This bug was fixed and documented here.

To see all tables, use the list subcommand to show the table and any contents. Shown below, all four table types are created and one entry is added to each table:

# ipfw table all list
#
# ipfw table foo create type addr
# ipfw table foo add 192.168.1.100
added: 192.168.1.100/32 0
#
# ipfw table bar create type iface
# ipfw table bar add em0
added: em0 0
#
# ipfw table baz create type number
# ipfw table baz add 9999
added: 9999 0
#
# ipfw table bop create type flow
# ipfw table bop add 203.0.113.10,192.168.1.100
ignored:  0
ipfw: Adding record failed: Invalid argument
#
# ipfw table bop destroy
#
# ipfw table bop create type flow:src-ip,dst-ip
# ipfw table bop add 203.0.113.10,192.168.1.100
added: 203.0.113.10,192.168.1.100 0
#
# ipfw table all list
--- table(bar), set(0) ---
em0 0
--- table(baz), set(0) ---
9999 0
--- table(bop), set(0) ---
203.0.113.10,192.168.1.100 0
--- table(foo), set(0) ---
192.168.1.100/32 0
#

Note the error on the flow table above. Flow tables take an explicit flow specification (discussed below) when they are created. If you try to add an entry to a flow table that does not match the flow specification, ipfw throws an error.

Note that the output list of tables is ordered by table name, not table type.

Tables can be removed one at a time with the destroy subcommand, provided the table is not used in any rule:

# ipfw table bar destroy

or removed all at once by specifying the special name all:

# ipfw table all destroy

Note that there is no confirmation with this command as there is with the flush command, so make sure that is really what you want to do.

3.4.2. Using Tables in Rules

First, on the firewall VM, create a table called goodhosts, and populate it with the addresses of hosts allowed to accept traffic:

# ipfw table goodhosts create type addr
# ipfw table goodhosts add 203.0.113.10/32
added: 203.0.113.10/32

Then create a rule that uses the table.

Note the single quotes around the table(goodhosts) entry to placate the shell:

# ipfw add 2000 allow udp from 'table(goodhosts)' to me
02000 allow udp from table(goodhosts) to me

To test, start up userv3.sh on the firewall VM:

# sh userv3.sh
Starting UDP listeners on [5656],[5657],[5658]

And on the external1 VM, start up ucon.sh:

# sh ucon.sh 5656
UDP communicationing [203.0.113.50],[5656],[1]
ncat [2] ready. Enter a valid PORTNUM:  5657
UDP communicationing [203.0.113.50],[5657],[2]
ncat [3] ready. Enter a valid PORTNUM:  5658
UDP communicationing [203.0.113.50],[5658],[3]
ncat [4] ready. Enter a valid PORTNUM:  ^C

If you remove the entry for 203.0.113.10/32 from the goodhosts table, the communications fail to reach the userv3.sh services listening on the firewall VM:

# ipfw table goodhosts delete 203.0.113.10
deleted: 203.0.113.10/32 0

Retrying the ucon.sh communications above will not be successful.

The next example uses the lookup keyword on table goodports. Start from scratch by unloading and reloading ipfw from the kernel. This ensures we don’t leave any cruft lying around:

# kldunload ipfw
# kldload ipfw

Next, create a table of type number and populate it with ports 5656 and 5657, but not 5658:

# ipfw table goodports create type number
#
# ipfw table goodports add 5656
added: 5656 0
#
# ipfw table goodports add 5657
added: 5657 0
#
# ipfw table goodports list
5656 0
5657 0
#

The lookup keyword searches for its parameter in a specified table. If the entry is found, the match succeeds and tablearg is set to the value returned from the table. Create a rule that uses this table with the lookup keyword:

# ipfw add 500 allow udp from 203.0.113.10 to me lookup dst-port goodports
00500 allow udp from 203.0.113.10 to me dst-ip lookup dst-port goodports

On the external1 VM, start up the ucon.sh script, and enter ports 5656, 5657, 5658, 5659, and 5656:

# sh ucon.sh 5656
UDP communicationing [203.0.113.50],[5656],[1]
ncat [2] ready. Enter a valid PORTNUM:  5657
UDP communicationing [203.0.113.50],[5657],[2]
ncat [3] ready. Enter a valid PORTNUM:  5658
UDP communicationing [203.0.113.50],[5658],[3]
ncat [4] ready. Enter a valid PORTNUM:  5659
UDP communicationing [203.0.113.50],[5659],[4]
ncat [5] ready. Enter a valid PORTNUM:  5656
UDP communicationing [203.0.113.50],[5656],[5]
ncat [6] ready. Enter a valid PORTNUM:  ^C

The results on the firewall host are:

# sh userv3.sh
Starting UDP listeners on [5656],[5657],[5658]
UDP communication from [203.0.113.10],[5656],[1]
UDP communication from [203.0.113.10],[5657],[2]
UDP communication from [203.0.113.10],[5656],[5]
^C

The two ports 5658 and 5659 were not in the table and thus not matched by rule 500.

It would be tempting to combine the previous two examples into something like:

# ipfw add 1000 allow udp from 'table(goodhosts)' to me dst-port 'table(goodports)'
ipfw: invalid destination port table(goodports)

but ipfw does not allow the use of more than one table in a rule.

However, instead of a second table keyword, you could use the lookup keyword for the port:

# ipfw add 500 allow udp from 'table(goodhosts)' to me lookup dst-port goodports
00500 allow udp from table(goodhosts) to me dst-ip lookup dst-port goodports

While does work, the better solution is to use the a flow table:

# ipfw table goodflow create type flow:src-ip,dst-port
#
# ipfw table goodflow add 203.0.113.10,5656
added: 203.0.113.10,5656 0
#
# ipfw table goodflow add 203.0.113.10,5657
added: 203.0.113.10,5657 0
#
# ipfw add 500 allow udp from any to me flow 'table(goodflow)'
00500 allow udp from any to me flow table(goodflow)

This gives you much more granular control of exactly what host and what port you want to match together.

Using the valtype keyword for addr tables you can have separate tables for IPv4 and IPv6:

# Create the translation tables.
# ipfw table T46 create type addr valtype ipv6
# ipfw table T64 create type addr valtype ipv4

3.4.2.1. Understanding the word tablearg

A tablearg is a computed value that is the result of a table lookup.

In the manual page ipfw(8), the word tablearg is used in two ways:

  • The term tablearg is used to show where in the rule the computed lookup will be applied. In this usage tablearg is just a placeholder.

  • In a rule, the actual word tablearg becomes the computed value taken from the last table lookup. This is typically described as the tablearg keyword in the documentation.

We have already seen the first usage when using the lookup keyword example above.

Essentially

# ipfw add 500 allow udp from 203.0.113.10 to me lookup dst-port goodports

becomes

# ipfw add 500 allow udp from 203.0.113.10 to me 5656

Below is an example of the second usage using the skipto action and the actual word tablearg in a rule.

Restart ipfw:

# kldunload ipfw
# kldload ipfw

Next, create a number table holding our favorite ports 5656, 5657, 5658.

# ipfw table create goodports
# ipfw table goodports add 5656
# ipfw table goodports add 5657
# ipfw table goodports add 5658

Then add rules:

# ipfw add 400 skipto tablearg udp from 203.0.113.10 to me lookup dst-port goodports
# ipfw add 1000 allow icmp from me to any
# ipfw add 4000 count ip from any to any
# ipfw add 5656 allow udp from 203.0.113.10 to me
# ipfw add 6000 count ip from any to any

This ruleset uses the skipto action to jump immediately to the rule number if the number is found in the table goodports. In effect, the goodports table acts as a dispatch table for rule processing. If a packet comes in with port 5656, 5657, or 5658, the skipto action is applied and processing begins with the rule number found in the table.

To demonstrate, start up the userv3.sh scrip on the firewall, and run the ucon.sh script on exteranal1.

Below is a transcript showing what happened.

# ipfw table goodports list
5656 0
5657 0
5658 0
#
# ipfw show
00400 0 0 skipto tablearg udp from 203.0.113.10 to me dst-ip lookup dst-port goodports
01000 0 0 allow icmp from me to any
04000 0 0 count ip from any to any
05656 0 0 allow udp from 203.0.113.10 to me
06000 0 0 count ip from any to any
65535 0 0 deny ip from any to any
#

Now, start sh userv3.sh on the firewall console, listening for UDP packets on the three ports and run sh ucon.sh 5656 on the external1 VM. This will send one packet.

Show counters:

# ipfw show
00400 1 71 skipto tablearg udp from 203.0.113.10 to me dst-ip lookup dst-port goodports
01000 0  0 allow icmp from me to any
04000 1 71 count ip from any to any
05656 1 71 allow udp from 203.0.113.10 to me
06000 0  0 count ip from any to any
65535 0  0 deny ip from any to any

The counters show the skipto rule was matched, and ipfw walked the ruleset up to rule 5656. There was a match on 5656 the destination rule, the userv3.sh received a UDP packet.

Next, send a UDP packet with an outlier port, 9000.

The counters now reveal:

# ipfw show
00400 1  71 skipto tablearg udp from 203.0.113.10 to me dst-ip lookup dst-port goodports
01000 1  99 allow icmp from me to any
04000 2 142 count ip from any to any
05656 2 142 allow udp from 203.0.113.10 to me
06000 0   0 count ip from any to any
65535 0   0 deny ip from any to any

The packet was not matched at rule 400. ipfw then walked the ruleset and found a matching rule at 4000 and next at 5656 where the packet was allowed through. However, rule 1000 shows that since there was no service listening on port 9000, the firewall sent an ICMP destination port unreachable packet back to the originator (external1).

From the ipfw(8) man page - The tablearg argument can be used with the following actions: nat, pipe, queue, divert, tee, netgraph, ngtee, fwd, skipto, setfib; wth action parameters: tag, untag; and with rule options: limit, tagged.

3.4.2.2. More on flow tables

A flow table maintains a list of flows. A flow is a designation given to traffic between two endpoints. The designation can be any of:

  • src-ip

  • src-port

  • proto

  • dst-ip

  • dst-port

or any combination that makes sense in source → destination order. Examples include:

# ipfw table zoo create type flow:src-ip // Creates a flow based on soure IP address
# ipfw table zar create type flow:proto  // Creates a flow based on just a protocol. *
# *ipfw table zaz create type flow:dst-ip // Creates a flow based on just the desitination IP
# ipfw table zop create type flow:dst-port // Creates a flow based on the destination port

or with extra specificity:

# ipfw table zip create type flow:src-ip,proto,dst-ip,dst-port // Flow based on all four
# ipfw table zim create type flow:src-ip,proto  // Just source IP and protocol
# ipfw table zam create type flow:src-ip,proto,dst-ip  // Source IP, dest. IP and protocol
# ipfw table zap create type flow:src-ip,dst-ip  // Just IP address endpoints

Once the table is created for a flow, entries can be placed in the table provided they match the table flow specification.

Match these additions to the tables created above:

# ipfw table zim add 192.168.200.30,tcp

would succeed, but

# ipfw table zip add 192.168.30.20,172.20.15.20

would fail because the flow specification for table zip is "src-ip,proto,dst-ip,dst-port" and neither the protocol nor the destination port was given.

A correct flow specification for table zip would be something like:

# ipfw table zip add 192.168.30.20,tcp,172.20.15.20,80

flow tables allow for precise definition of traffic between a source and a destination. Once in the table, rules can be applied to commands allow, deny, divert, queue, etc. for modifying traffic flow.

General notes on all tables:

  • Table names can be numeric or alphanumeric and can include only hyphen (-), underscore (_), and period (.) as special characters.

  • Maximum table name length is 63 characters.

  • Tables names must be unique within a set. Tables can have the same name across different sets, however any rules for tables in sets other than set 0, must include the set number. The sysctl variable net.inet.ip.fw.tables_sets controls this behavior.

  • The maximum number of tables across all sets is 65535. Practically however, the number is controlled by the sysctl variable 'net.inet.ip.fw.tables_max'. The default is 128.

  • If a table is in use in a rule, it cannot be destroyed. The rule must be removed first, then the table can be destroyed. However, a table can be flushed at anytime.

  • All table types survive a flush action, and table contents are not affected.

  • Make table names as descriptive as possible to avoid confusion when used in rules. The names we are using here (foo, bar, zip, zap, etc.) are just examples.

  • As noted in the man page, if two tables are used in a rule, the result of the second (destination) is used. Therefore avoid using two tables in a rule, or try using the lookup keyword instead.

Notes on specific table types:

  • Address tables (addr)

    • Address tables (addr) support IPv4 and IPv6 address types, and varying mask lengths appropriate for each address type. The default mask length for IPv4 is 32 bits (/32) and the default for IPv6 is 128 bits (/128).

    • Table lookups will return the most specific entry, so 203.0.113.20/32 is preferred over 203.0.113.0/24.

  • Interface tables (iface)

    • Interface tables store interface names as alphanumeric text. The text does not actually have to match a current valid interface.

    • Special characters in interface names can include any from the set

      [-+_?,.!~@#%^&*()=;:/,<>{}|]

      Note however, that the shell may recognize some of these characters when adding and during lookup, thus interfering with the table operation, and so special characters in interface names should be avoided.

    • The maximum key length is 15 characters.

    • There is no support for interface ranges i.e. em0-4, even though an interface name "em0-4" can be entered.

  • Number tables (number)

    • Number tables support unsigned 32-bit integer types.

    • Entries can be positive or negative. Negative entries perform unsigned ones complement arithmetic, and positive numbers roll over from 4294967295 to 0. So don’t do that.

    • Any shell element that evaluates to a number can be used: shell variables that resolve to a number ($MAILCHECK, $PPID), backtick operations such as expr 5 + $UID, id -u, date "+%s", and special variables like $RANDOM in bash.

    • As with other table types, ranges are not supported.

  • Flow tables (flow)

    • Flow tables describe network traffic based on the desired attributes. The best matches include as much detail as possible: src-ip, proto, dst-ip, port. Including less than that may make it difficult to add an element in the table:

      • ipfw table foo create flow:dst-port // Table based on just the destination port

      • ipfw table foo add telnet // Fails to add!

    • ipfw table bar create flow:dst-ip,dst-port

    • ipfw table bar add 203.0.113.10,5656

3.5. Stream Control Transport Protocol (SCTP)

A readable introduction to SCTP is found in Wikipedia - https://en.wikipedia.org/wiki/Stream_Control_Transmission_Protocol#RFCs and ever more detail is found in the accompanying RFCs.

3.5.1. SCTP Versions

As of FreeBSD 14.1, there are three different versions of SCTP - a "native" version, a "separate portable" version, and a "userland" version.

  • FreeBSD ships with the "native" version of the protocol, described in sctp(4). (This is actually the reference implementation of SCTP initially developed on FreeBSD 7.) This version requires loading the kernel module sctp.ko before use. Native SCTP uses the SCTP library on FreeBSD which provides access to the various functions found in <netinet/sctp.h>. You can use the features described in sctp(4) to develop SCTP applications based on the native protocol.

Then, there are a couple of notable packages regarding SCTP:

  • libusrsctp “Portable SCTP userland stack” – This is a non-kernel implementation of the protocol using “usrsctp_*” functions. It also provides UDP encapsulation as shown in the Wireshark image below:

  • sctplib “User-space implementation of the SCTP protocol RFC 4960” – This package is a re-implementation of the native SCTP code that can function as a replacement for the default installed code. It does not require the sctp.ko kernel module.

As noted in the BUGS section of sctp(4), the sctp.ko kernel module cannot be unloaded. You will have to restart FreeBSD to remove the module from the kernel.

Interestingly, the reference implementation for SCTP that was developed on FreeBSD 7 and has been archived on Github here. ipfw played a role in its development and the vestiges of that development remain in the ipfw code base and in the collection of sysctls that support it.

We will study the native SCTP protocol usage and the encapsulated usage with ipfw.

3.5.2. SCTP Protocol Operation

The image below shows a Wireshark view of a typical native SCTP association (connection):

.Wireshark view of Native SCTP
Figure 8. Wireshark View of Native SCTP

Notice how there are two different interfaces involved with this transfer - 127.0.0.1, and 192.168.1.78. In fact, SCTP supports association setup and data exchange with multiple interfaces to a remote node. The RFC’s explain this capability in detail.

Also in this image, you can see the basic 4-way handshake - INIT, INIT_ACK, COOKIE_ECHO, and COOKIE_ACK, the HEARTBEAT, DATA, SACK (Stream Acknowledgement) messages, and the shutdown sequence of SHUTDOWN, SHUTDOWN_ACK, and SHUTDOWN_COMPLETE.

Below is a view of netstat -an showing the display of a separate SCTP section containing all current associations:

% netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address          Foreign Address        (state)
tcp4       0      0 *.22                   *.*                    LISTEN
tcp6       0      0 *.22                   *.*                    LISTEN
tcp4       0      0 127.0.0.1.631          *.*                    LISTEN
tcp6       0      0 ::1.631                *.*                    LISTEN
udp4       0      0 127.0.0.1.123          *.*
udp6       0      0 fe80::1%lo0.123        *.*
udp6       0      0 ::1.123                *.*
udp4       0      0 192.168.1.78.123       *.*
udp6       0      0 2600:1700:3901:4.123   *.*
udp6       0      0 fe80::3e97:eff:f.123   *.*
udp4       0      0 *.123                  *.*
udp6       0      0 *.123                  *.*
Active SCTP associations (including servers)
Proto  Type  Local Address          Foreign Address        (state)
sctp4  1to1  127.0.0.1.5000         127.0.0.1.42227        ESTABLISHED
             192.168.1.78.5000      192.168.1.78.42227
sctp4  1to1  127.0.0.1.42227        127.0.0.1.5000         ESTABLISHED
             192.168.1.78.42227     192.168.1.78.5000
sctp4  1to1  127.0.0.1.5000                                LISTEN
             192.168.1.78.5000
Active UNIX domain sockets
Address          Type   Recv-Q Send-Q            Inode             Conn             Refs          Nextref Addr
fffff8015575c000 stream      0      0                0 fffff80155758c00                0                0
. . .

Internally, the SCTP data packets look different from TCP and/or UDP packets. However, like those two, SCTP has a protocol number, source address, destination address, and port numbers, so using ipfw with SCTP will be fairly straightforward.

Here’s a look at a typical packet:

.Internal View of the SCTP INIT Data Packet
Figure 9. Internal View of the SCTP INIT Data Packet

To study SCTP’s behavior with ipfw we will use the client/server echo program, shown above. We will also look at a streaming application based on the chargen small server of yore.

3.5.3. Using the TSCTP Testing Tool on FreeBSD

A handy testing tool is the FreeBSD package tsctp. Install the package on the external1 and internal VMs. (You will have to configure the VMs to access the Internet to install the package. Refer to the Quick Start section for details.)

# pkg install tsctp

This tool uses the native SCTP protocol version. To use, we first load the sctp.ko kernel module:

# *kldload stcp.ko

Running the program is discussed below.

To get started, initialize the bridge and tap devices to the same architecture as "Simple NAT", shown in the Figure below.

.Setting Up to Test Native SCTP
Figure 10. Setting Up to Test Native SCTP
% sudo /bin/sh mkbr.sh reset bridge0 tap1 tap4 bridge1 tap0 tap5

Then start the VMs we need:

% /bin/sh runvm.sh firewall external1 internal

Then follow these steps:

  1. Apply the addressing shown in the above figure and ensure connectivity with adjacent interfaces, and with the opposite side interfaces (10.10.10.20 from the external1 VM and 203.0.113.10 from the internal VM).

  2. The default route for the external1 VM should point to 203.0.113.50, and for the internal VM it should point to 10.10.10.50.

  3. Load the sctp.ko kernel module on the external1 and the internal VMs. This enables communication via native SCTP on these VMs.

    On the internal VM, run in server mode:
    
        tsctp -L 127.0.0.1 -L 10.10.10.20 -p 1234
    
    and on the external1 VM, run in client mode:
    
        tsctp  -L 203.0.113.10 -p 1234 -n 10 -l 1000 192.168.1.128

Use tscp --help to get a list of the options and meanings.

The client program sends 10 messages (-n 10) to the server. Simple statistics are shown once the program terminates.

With the above procedure, we have established SCTP communications across the firewall. The firewall VM does not need to load the sctp.ko module. To the firewall VM, this is simply normal IP traffic.

Restart the client with -n 0, for unlimited messages. While normal SCTP traffic is established from client to server, load ipfw on the firewall VM.

Traffic is immediately halted. Eventually, the client will recognize it has been disconnected. This may take a couple of minutes. The disconnection is shown in the image below:

.IPFW Firewall Disrupts SCTP Traffic
Figure 11. IPFW Firewall Disrupts SCTP Traffic

Creating suitable rules for SCTP traffic is quite similar to other rules we have performed before. You can use the "two rule" version:

.IPFW Traffic Flow With Two Rules
Figure 12. IPFW Traffic Flow With Two Rules

One would think the "one-rule" version using setup and keep-state keywords would work, but it does not. You have to use the "two-rule" version:

root@firewall:~ # ipfw show
00100  0    0 check-state :default
01000 40 4152 allow sctp from 203.0.113.10 to 10.10.10.20
02000 48 4424 allow sctp from 10.10.10.20 to 203.0.113.10
65535  0    0 deny ip from any to any
root@firewall:~ #

Also, keep in mind that SCTP is typically used where there are multiple interfaces per association. This example has used only one, but the principles are the same.

3.5.4. Downloading and Building usrsctp Programs

To test SCTP encapsulation with UDP, we must download and build the usrsctp kit. Follow this procedure to download and build the programs for the usrsctp kit. On the external1 VM and the internal VM, the procedure is the same.

For this procedure, you will have to reconfigure the external1 and internal VMs to access the Internet as described in Quick Start.

# pkg install git
# pkg install cmake
# mkdir /root/src
# cd /root/src
# git clone https://github.com/sctplab/usrsctp.git
# mkdir tmp
# cd tmp
# cmake ../usrsctp
# cmake --build .

Once finished, the test programs are located in /root/src/tmp/programs.

Now reconfigure external1 and internal VMs to the architecture in Figure 10.

3.5.5. Encapsulated Echo Server and Client with IPFW

Figure 13 shows encapsulated usage of SCTP with the chargen_server_upcall program running on the internal VM and the client_upcall program running on the external1 VM. The output is similar to that of the chargen small server program.

.SCTP Traffic Encapsulated in UDP Datagrams
Figure 13. SCTP Traffic Encapsulated in UDP Datagrams

All the data exchanged between the two systems was encapsulated in UDP datagrams as shown in Figure 14.

.Wireshark View of UDP Encapsulation of SCTP
Figure 14. Wireshark View of UDP Encapsulation of SCTP

Any ipfw rules for this traffic only have to be concerned with UDP, not SCTP.