courses

Defensive Measures

Deciding what traffic to allow — and watching the rest

A firewall answers one question for every packet: should this be allowed through? That decision is the most fundamental network defense there is — it is access control applied to traffic instead of files. But filtering alone only stops what you thought to block, so a complete posture pairs packet filtering (firewalls) with detection (intrusion detection/prevention) that inspects the traffic you did let through.

This page works through both halves. On the filtering side it covers four rule engines you will actually meet — Linux’s iptables/nftables, BSD’s pf, and FreeBSD’s ipfw — each translating the same policy so you can see how the model carries across platforms. On the detection side it covers Suricata rules (including Lua), and finishes with the all-in-one “firewall distros” and monitoring appliances that package these pieces behind a web UI.

ℹ️ Two themes recur across every engine below: rule order matters, and the default policy is your real security boundary. Read each ruleset asking “what happens to a packet that matches nothing?”

Firewalls

Firewalls are a classic defensive measure. Designed as a tool to limit inbound or outbound connections, firewalls are traditionally rule based. We will primarily be discussing the Linux iptables instantiation of a firewall, mainly due to the ease of access – it’s on the Kali VM we have been using all term.

At the end of the day, firewalls are nothing but sets of rules. These rules govern which packets are kept, and which packets get dropped like a bad habit. One of the most important things to keep in mind is that order matters!

Firewall rules

As mentioned above, firewalls are simply a set of rules. If you’d like to see the current rule-set, execute the below

┌─(ROOT@kali478-0:pts/4)───────────────────────────────────────────────────────────────────(~/cs478)─┐
└─(2:12:43:#)── iptables --list                                                        ──(Fri,Feb14)─┘
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

As you can see, within Kali, the defaults are empty. If you want to know what else iptables can do, take a look at the help:

┌─(ROOT@kali478-0:pts/4)───────────────────────────────────────────────────────────────────(~/cs478)─┐
└─(2:12:43:#)── iptables --help                                                        ──(Fri,Feb14)─┘
iptables v1.8.3

Usage: iptables -[ACD] chain rule-specification [options]
	iptables -I chain [rulenum] rule-specification [options]
	iptables -R chain rulenum rule-specification [options]
	iptables -D chain rulenum [options]
	iptables -[LS] [chain [rulenum]] [options]
	iptables -[FZ] [chain] [options]
	iptables -[NX] chain
	iptables -E old-chain-name new-chain-name
	iptables -P chain target [options]
	iptables -h (print this help information)

Commands:
Either long or short options are allowed.
  --append  -A chain		Append to chain
  --check   -C chain		Check for the existence of a rule
  --delete  -D chain		Delete matching rule from chain
  --delete  -D chain rulenum
				Delete rule rulenum (1 = first) from chain
  --insert  -I chain [rulenum]
				Insert in chain as rulenum (default 1=first)
  --replace -R chain rulenum
				Replace rule rulenum (1 = first) in chain
  --list    -L [chain [rulenum]]
				List the rules in a chain or all chains
  --list-rules -S [chain [rulenum]]
				Print the rules in a chain or all chains
  --flush   -F [chain]		Delete all rules in  chain or all chains
  --zero    -Z [chain [rulenum]]
				Zero counters in chain or all chains
  --new     -N chain		Create a new user-defined chain
  --delete-chain
	     -X [chain]		Delete a user-defined chain
  --policy  -P chain target
				Change policy on chain to target
  --rename-chain
	     -E old-chain new-chain
				Change chain name, (moving any references)
Options:
    --ipv4	-4		Nothing (line is ignored by ip6tables-restore)
    --ipv6	-6		Error (line is ignored by iptables-restore)
[!] --proto	-p proto	protocol: by number or name, eg. 'tcp'
[!] --source	-s address[/mask][...]
				source specification
[!] --destination -d address[/mask][...]
				destination specification
[!] --in-interface -i input name[+]
				network interface name ([+] for wildcard)
 --jump	-j target
				target for rule (may load target extension)
  --goto      -g chain
			       jump to chain with no return
  --match	-m match
				extended match (may load extension)
  --numeric	-n		numeric output of addresses and ports
[!] --out-interface -o output name[+]
				network interface name ([+] for wildcard)
  --table	-t table	table to manipulate (default: 'filter')
  --verbose	-v		verbose mode
  --wait	-w [seconds]	maximum wait to acquire xtables lock before give up
  --wait-interval -W [usecs]	wait time to try to acquire xtables lock
				default is 1 second
  --line-numbers		print line numbers when listing
  --exact	-x		expand numbers (display exact values)
[!] --fragment	-f		match second or further fragments only
  --modprobe=<command>		try to insert modules using this command
  --set-counters PKTS BYTES	set the counter during insert/append
[!] --version	-V		print package version.

So let’s add some rules:

❯ iptables -A OUTPUT -p tcp -d ubuntu.com -j ACCEPT
❯ iptables -A OUTPUT -p tcp -d ca.archive.ubuntu.com -j ACCEPT
❯ iptables -A OUTPUT -p tcp --dport 80 -j DROP
❯ iptables -A OUTPUT -p tcp --dport 443 -j DROP
❯ iptables -A INPUT -p tcp -s 172.16.0.45 --dport 22 -j ACCEPT
❯ iptables -A INPUT -p tcp -s 0.0.0.0/0 --dport 22 -j DROP

What do these rules do? Let’s break them down:

  1. Allow all traffic to ubuntu.com
  2. Allow all traffic to ca.archive.ubuntu.com
  3. Drop all traffic destined for port 80
  4. Drop all traffic destined for port 443
  5. Allow traffic from 172.16.0.45 to port 22
  6. Drop all traffic from any other source to port 22

So we can SSH in, we can update the system, but we can’t browse the web. This is a very basic example of how iptables can be used to control traffic.

Rule processing

Rules are processed in numerical order, from first to last. Processing stops as soon as a rule is matched – ORDER MATTERS! There are ways to move rules around in the ipchains, but it’s better to just enter them in the correct order.

nftables

nftables is the successor to iptables and has been the default firewall framework on Debian-based systems (including Kali) since Debian 10. It replaces not just iptables but the entire collection of legacy tools — iptables, ip6tables, arptables, and ebtables — with a single unified front end: nft.

The kernel-side engine compiles rules into bytecode at the time they are loaded rather than interpreting them at packet time, which gives nftables a performance edge over iptables at high packet rates.

Structure: tables, chains, and rules

nftables organizes configuration in three layers:

Layer Purpose
Table Top-level namespace; has an address family (ip, ip6, inet, arp, bridge, netdev)
Chain Container for rules; a base chain hooks into the netfilter path; a regular chain is a jump target
Rule Match expression + verdict (accept, drop, reject, jump, log, …)

Unlike iptables, no tables or chains exist by default — you create only what you need. The inet family handles IPv4 and IPv6 with a single rule set, which is the most common choice for host firewalls.

Viewing the current ruleset

❯ nft list ruleset

On a fresh Kali install this produces no output — the ruleset is empty.

Basic nft commands

Operation Command
List everything nft list ruleset
Add a table nft add table inet filter
Add a base chain nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }'
Add a rule nft add rule inet filter input tcp dport 22 accept
Insert a rule at position 0 nft insert rule inet filter input tcp dport 22 accept
List rules with handles nft list chain inet filter input
Delete a rule by handle nft delete rule inet filter input handle 4
Flush all rules nft flush ruleset

Worked example

The iptables rules from the section above — allow SSH from one host, allow apt updates, block all other web traffic — translate to nftables as follows:

# Create a table and base chains
❯ nft add table inet filter
❯ nft add chain inet filter input  '{ type filter hook input  priority 0; policy accept; }'
❯ nft add chain inet filter output '{ type filter hook output priority 0; policy accept; }'

# OUTPUT: allow apt traffic to specific hosts, then drop ports 80 and 443
❯ nft add rule inet filter output ip daddr ubuntu.com            tcp accept
❯ nft add rule inet filter output ip daddr ca.archive.ubuntu.com tcp accept
❯ nft add rule inet filter output tcp dport { 80, 443 }          drop

# INPUT: allow SSH from one host, drop it from everyone else
❯ nft add rule inet filter input ip saddr 172.16.0.45 tcp dport 22 accept
❯ nft add rule inet filter input                      tcp dport 22 drop

Verify the result:

❯ nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        ip saddr 172.16.0.45 tcp dport 22 accept
        tcp dport 22 drop
    }

    chain output {
        type filter hook output priority filter; policy accept;
        ip daddr ubuntu.com tcp accept
        ip daddr ca.archive.ubuntu.com tcp accept
        tcp dport { 80, 443 } drop
    }
}

The { 80, 443 } syntax — called a set literal — lets you match multiple values in a single rule, replacing two separate iptables rules.

Persistent rules

Rules added with nft are lost on reboot. Persist them by saving the ruleset and loading it at boot:

# Save current ruleset
❯ nft list ruleset > /etc/nftables.conf

# Restore manually (or from a startup script)
❯ nft -f /etc/nftables.conf

On systemd-based systems the nftables service does this automatically:

❯ systemctl enable --now nftables

Translating existing iptables rules

iptables-translate converts individual iptables rules to their nftables equivalent:

❯ iptables-translate -A INPUT -p tcp -s 172.16.0.45 --dport 22 -j ACCEPT
nft add rule ip filter INPUT ip saddr 172.16.0.45 tcp dport 22 counter accept

iptables-nft is a drop-in iptables replacement that stores rules in the nftables kernel engine while accepting the old syntax — useful during a gradual migration.

pf

pf (Packet Filter) originated in OpenBSD and is the default firewall on all BSDs. It is also the engine under the hood of pfSense and OPNsense. macOS ships with pf as well. The command-line tool for managing pf is pfctl.

Rule evaluation

pf reads its configuration from /etc/pf.conf and evaluates rules top to bottom, but the last matching rule wins — the opposite of iptables and ipfw. The quick keyword overrides this: when a packet matches a quick rule, evaluation stops immediately at that rule.

# Without quick: last match wins
block all
pass proto tcp to any port 22      # ← this wins for SSH traffic

# With quick: first match stops processing
pass  quick on lo0 all             # ← loopback stops here
block all                          # ← never reached for lo0 packets

pfctl commands

Operation Command
Enable pf pfctl -e
Disable pf pfctl -d
Load rules pfctl -f /etc/pf.conf
Dry run (syntax check) pfctl -nf /etc/pf.conf
Show active rules pfctl -sr
Show state table pfctl -ss
Show statistics pfctl -si
Flush rules pfctl -F rules

Worked example

The same policy as the iptables example — SSH from one host only, apt updates allowed, all other HTTP/HTTPS blocked — as a pf.conf:

# /etc/pf.conf

# Named table: resolved once at load time
table <apt_hosts> const { ubuntu.com, ca.archive.ubuntu.com }

# Default deny
block all

# Loopback is always trusted
pass quick on lo0 all

# SSH: allow from one host, block from everywhere else
pass  in quick proto tcp from 172.16.0.45 to any port 22 keep state
block in quick proto tcp from any         to any port 22

# Apt updates to specific hosts only
pass out quick proto tcp to <apt_hosts> port { 80, 443 } keep state

# Block all other outbound HTTP/HTTPS
block out proto tcp to any port { 80, 443 }

A pf table (<apt_hosts>) is a named set of addresses. You can add or remove members at runtime without reloading the full ruleset:

pfctl -t apt_hosts -T add 1.2.3.4
pfctl -t apt_hosts -T delete 1.2.3.4
pfctl -t apt_hosts -T show

Loading rules at boot

On OpenBSD, enable in /etc/rc.conf.local:

pf=YES

On FreeBSD, add to /etc/rc.conf:

pf_enable="YES"
pf_rules="/etc/pf.conf"

ipfw

ipfw is FreeBSD’s built-in stateful firewall. It uses first-match-wins semantics — the same as iptables. Rules are numbered 1–65535; rule 65535 is the built-in implicit deny all and cannot be deleted.

Rule syntax

ipfw add [rulenum] action [log] proto from src [port] to dst [port] [options]

Common actions: allow (aliases: accept, pass), deny (alias: drop), reject, count, skipto

ipfw commands

Operation Command
List all rules ipfw list
List with byte/packet counts ipfw -a list
Add a rule ipfw add 100 allow tcp from any to me dst-port 22
Delete rule 100 ipfw delete 100
Flush all rules ipfw -f flush
Show dynamic state table ipfw show

Worked example

# Flush existing rules
ipfw -q -f flush

# Rule 100: loopback — always allow
ipfw add 100 allow all from any to any via lo0

# Rule 200: allow SSH from one host
#   keep-state installs a dynamic rule for return traffic
ipfw add 200 allow tcp from 172.16.0.45 to me dst-port 22 keep-state

# Rules 300–310: allow outbound apt traffic to specific hosts
ipfw add 300 allow tcp from me to ubuntu.com            keep-state
ipfw add 310 allow tcp from me to ca.archive.ubuntu.com keep-state

# Rule 400: block all other outbound HTTP/HTTPS
ipfw add 400 deny tcp from me to any dst-port 80,443

# Rule 65535 (implicit): deny all — already present, cannot be deleted

keep-state installs a dynamic rule that tracks the connection and automatically permits reply packets, so no explicit inbound allow is needed for established sessions.

Persistent rules

Write rules to a script and reference it in /etc/rc.conf:

firewall_enable="YES"
firewall_script="/etc/ipfw.rules"

Intrusion Detection/Prevention Systems

Suricata is an open source (with commercial support) network based intrusion detection/prevention system (NIDS/NIPS). Much like the Snort tool, it is a production grade NIPS that has significant community support. For our purposes, Suricata is the example used due to its support of rule expansions via the Lua scripting language. Snortv3 also now supports Lua scripts in rules, but is not well documented (to my knowledge).

Suricata rules

From the Suricata manual:

A rule/signature consists of the following:

* The action, that determines what happens when the signature matches
* The header, defining the protocol, IP addresses, ports and direction of the rule.
* The rule options, defining the specifics of the rule.
#action protocol IPs ports -> IPs ports (trigger conditions)

drop tcp 10.0.0.0/8 any -> $HOME_NET any (msg:"dropping all traffic from non-routable IPs"; sid:20201234; rev:2;)

The above has both the general format, as well as an example rule. sid is required, while rev is recommended.

Built-in rules

Suricata comes with a significant batch of pre-built rules for a lot of the common network packets you might want to filter out. These include unwanted pings, network scan requests, ICMP traffic at all, filtering for known exploit traffic, etc. Further, there are some packet “classes” you can use in rules that encapsulate fairly complex rule logic.

Custom rules

Sometimes though, the built in rules just won’t cut it. Maybe there’s a new exploit in the wild. Maybe you want to alert on specific actions from smb. Maybe you just want to explore. There are several ways to add custom rules:

The above list is not exhaustive, and I want us to focus primarily on the final point. Lua gives us the ability to perform complex logic on our packet structure or the content of the packet. It allows for things like detecting a mismatch between reported and actual length (remember that, it’ll be useful soon). The below verifies that reported length is the same as actual length.

 1 function init (args)
 2     local needs = {}
 3     needs["payload"] = tostring(true)
 4     return needs
 5 end
 6 
 7 function string.tohex(str)
 8     return (str:gsub('.', function (c)
 9         return string.format('%02X ', string.byte(c))
10     end))  
11 end
12 
13 function match(args)
14     local b = args['payload']
15     if b == nil then
16         print ("Payload buffer empty! Aborting...")
17         return 0
18     end
19     print (string.tohex(b))
20     -- DNS RFC specifies length is reported in the first two bytes.
21     dns_size_high_byte = b:byte(1)
22     dns_size_low_byte = b:byte(2)
23     dns_size = tonumber(dns_size_high_byte) * 256 + tonumber(dns_size_low_byte)
24     -- check to ensure reported lenghth is same as actual length.  Subtract 2 for length field
25     if dns_size ~= string.len(b)-2 then
26             return 1
27     end
28     return 0
29 end

Lines 1-5 are required, while lines 7-11 are useful for debugging purposes. On a failed test, return 1 to trigger the rule. On a passing test, return 0 to skip this rule. Pay attention to lines 21-23 – this is how you pull out bytes and combine into a 16-bit int.

Optional Additional Resources

If this topic interests you, then you might find the following optional resources useful.

There exist some operating systems that are designed to be firewalls. These are often referred to as “firewall distros”. Some (but certainly not all) of the more popular ones are:

The first two are BSD based, while the rest are Linux based. Their capabilities vary, but they all offer, at minimum, a firewall and intrusion detection/prevention system. They also often have VPN capabilities and a user-friendly web interface for configuration.

Security Appliances

The firewalls and tools above are typically deployed on general-purpose systems managed from the command line. Several projects package these capabilities into purpose-built distributions designed to run on dedicated hardware or VMs and managed entirely through a web interface.

OPNsense

OPNsense is a FreeBSD-based firewall and routing platform forked from pfSense in 2015 and maintained by Deciso. pf is the packet filtering engine; the web UI exposes it without requiring any command-line work.

Core capabilities:

Feature Details
Firewall Stateful pf rules, NAT, traffic shaping, VLAN support
IDS/IPS Built-in Suricata with Proofpoint Emerging Threats rules
VPN WireGuard, OpenVPN, IPsec (route-based and policy-based)
DNS Unbound resolver with DNS-over-TLS
High availability CARP with automatic state synchronization
Automation REST API for scripted configuration

OPNsense releases on a predictable six-month cycle (January and July). It is a practical choice for a network perimeter device, a lab gateway, or anywhere you want pf-based enforcement without managing pf.conf by hand.

Security Onion

Security Onion is a Linux-based (Ubuntu) distribution designed for network security monitoring (NSM), threat hunting, and digital forensics and incident response (DFIR). It is not a traditional firewall — it is a detection and analysis platform.

Core components:

Component Role
Suricata Signature-based IDS/IPS and per-alert PCAP capture
Zeek Protocol metadata extraction and connection logging
Elasticsearch Log and metadata indexing and search
Kibana Dashboards and visualization
CyberChef In-browser data decoding and transformation
osquery Host-based endpoint telemetry
Security Onion Console Alerting, case management, and threat hunting UI

In a typical deployment a tap or SPAN port feeds all network traffic to the sensor; Suricata and Zeek process it in real time and write results into Elasticsearch for analysis. Security Onion scales from a single-node sensor on a laptop to a distributed grid.

It pairs well with OPNsense: OPNsense enforces policy at the network edge, Security Onion watches what passes through.

Key takeaways

References


Related course pages: Introduction to Networking · Capturing traffic with tcpdump · Analyzing traffic with Wireshark

🛠️ Maintenance note: tool versions drift — the iptables --help/--list output and the iptables v1.8.3 banner are from an older Kali; verify against the current image, and remember Kali now uses nftables underneath iptables-nft by default. OPNsense ships on a six-month cycle (January/July), and Suricata rule syntax and the Lua API evolve between major releases — re-check the linked docs each term.