tcpdump BPF Filters for Network Forensics

 


BPF filter expressions, using the pcap-filter syntax supported by tcpdump, enable forensic examiners to selectively capture, inspect, and extract packets from network traffic based on specified protocol fields, header values, and packet characteristics. The libpcap library compiles these expressions into packet-filtering instructions, traditionally classic Berkeley Packet Filter (cBPF) bytecode, which can be examined using tcpdump -d. During live packet acquisition, the compiled filter can be installed in the operating system's packet-capture mechanism. Where kernel-level filtering is supported, packets that do not satisfy the filter criteria are discarded before delivery to userspace, reducing unnecessary processing and capture overhead. When examining an existing packet capture using the -r option, libpcap ordinarily evaluates the filter against packets read from the capture file in userspace. Combined with the -w option, this functionality allows an examiner to generate a filtered derivative of the original capture containing only packets that satisfy the specified criteria, without modifying the source evidence. To maintain evidentiary integrity and analytical reproducibility, the original capture should be preserved, cryptographic hashes should be calculated and documented, and the filtering expression, tool version, processing parameters, and resulting output hash should be recorded. This establishes a traceable relationship between the original evidence and the derived dataset.


BPF filtering is particularly useful during the preliminary triage and targeted examination of large packet captures, where investigators need to isolate traffic associated with specific network activities or indicators of interest. Consider a packet capture containing evidence of a TCP SYN scan. One of the initial analytical objectives is to identify the responses associated with the probed ports and determine which ports exhibit characteristics consistent with an open TCP endpoint. In conventional TCP SYN scanning, a target that accepts an incoming connection request typically responds with a SYN-ACK segment, whereas a closed port generally responds with an RST-ACK segment. A filtered port may produce no observable response or return an ICMP error message indicating that communication is administratively or otherwise prohibited. To isolate SYN-ACK responses originating from the scanned host and directed towards the scanning system, an examiner can apply the following filter:


tcpdump -nn -r capture.pcap -w synack.pcap 'dst host <scanner> and tcp[tcpflags] & (tcp-syn|tcp-ack) = (tcp-syn|tcp-ack)'


The expression selects TCP segments in which both the SYN and ACK flags are set, while restricting the destination address to the scanner. The use of bitwise masking, rather than an exact equality comparison, such as tcp[13] = 0x12, is important because additional TCP control flags may be present. For example, ECN negotiation can introduce the ECE flag into a SYN-ACK response. An exact comparison against 0x12 would exclude such packets, whereas the masking expression identifies segments containing both required flags regardless of additional flag bits.


A SYN-ACK response is conventionally interpreted as evidence that the responding TCP endpoint is accepting connection requests on the destination port. However, this observation should not be treated as conclusive proof that a particular application service is running on the scanned host. The response may originate from the host's TCP stack or an intermediary operating on its behalf, such as a firewall, load balancer, or SYN proxy. Furthermore, the interpretation must account for the network position of the capture sensor, potential packet loss, asymmetric routing, and other collection limitations that may affect the completeness of the observed traffic. Where available, corroboration with host-based evidence, service configurations, listening socket information, and relevant system logs strengthens the forensic conclusion.


It is important to note that the tcp[...], udp[...] and icmp[...] byte-offset accessors are defined for IPv4 only. The pcap-filter manual states that these upper-layer accessors do not apply to IPv6, so they should not be relied on to match or inspect IPv6 segments. Dual-stack captures need a separate IPv6 expression written against the 40-byte fixed header, in which Next Header is at offset 6 and the TCP flags byte is at offset 40 + 13 = 53:


'ip6[6] = 6 and ip6[53] & 0x12 = 0x12'


This matches IPv6 SYN-ACK segments only when TCP immediately follows the fixed header. If extension headers are present, Next Header names the first extension header and the TCP header shifts, so the packet is missed. To cover both families in one expression:


'(ip andtcp[13] & 0x12 = 0x12) or (ip6 and ip6[6] = 6 and ip6[53] & 0x12 = 0x12)'


Although many BPF expressions can be constructed using straightforward protocol and address matching, greater care is required when examining header fields that occupy only part of a byte. Such fields include the IPv4 version and Internet Header Length (IHL) nibbles, Differentiated Services Code Point (DSCP) and Explicit Congestion Notification (ECN) bits, IPv4 fragmentation flags and fragment offset, TCP data offset, and TCP control flags. Extracting or testing these values requires an understanding of the relevant header layout and the application of appropriate bitwise operations. For example, the expression ip[0] & 0x0f clears the upper four bits of the first IPv4 header byte, which hold the version, and retains the lower four bits, which hold the IHL field. Understanding these operations is fundamental to constructing precise filters for packet-level forensic examination.


The application of BPF extends beyond tcpdump. Wireshark and TShark support pcap-filter syntax for capture filters, although their display filters use a separate expression language and are not interchangeable with BPF capture filters. Network acquisition and packet-processing libraries, including Scapy, also support BPF-based filtering in appropriate capture environments. At the operating-system level, classic BPF programs can be attached to sockets using SO_ATTACH_FILTER, while extended BPF (eBPF) provides a more versatile programmable infrastructure for modern network monitoring, tracing, and observability. Consequently, an understanding of protocol-header structures, byte offsets, and bitwise masking is valuable not only for packet acquisition and network forensics but also for network programming, intrusion detection, and detection engineering.


A BPF filter expression specifies the packet field, byte offset, and value or bit pattern that an examiner intends to evaluate during packet acquisition or subsequent analysis. Depending on the protocol, the target field may occupy a single bit, a byte, or a multi-byte sequence within the packet header. Examples include the Internet Header Length (IHL) field in the IPv4 header, the TCP control flags, the UDP destination port, and the ICMP message type. BPF expressions can also inspect packet payload bytes at specified offsets. However, payload inspection requires careful consideration of preceding header lengths, encapsulation, and protocol structure, as these factors can make the payload position variable across packets.


The pcap-filter syntax provides protocol primitives such as ip, ip6, tcp, udp, and icmp to simplify the construction of common packet-matching criteria. These primitives are convenient abstractions rather than mandatory components of a filter expression. For example, within IPv4 traffic, ip proto 6 is equivalent to ip[9] = 6, because the IPv4 Protocol field is located at byte offset 9 within the IPv4 header, and the value 6 identifies TCP. However, the bare tcp primitive is broader because it can also match TCP traffic carried over IPv6. For direct header-field inspection, pcap-filter supports the indexed access notation protocol[offset:size], where the offset is zero-based and the size specifies the number of bytes to retrieve: 1, 2, or 4 bytes, with 1 byte as the default. For example, tcp[13] references the TCP control flags byte, located at offset 13 relative to the beginning of the TCP header.


An examiner can alternatively construct a filter using explicit link-layer offsets, provided the encapsulation structure and header positions are known. In a conventional Ethernet II frame without VLAN tagging, the EtherType field occupies bytes 12 and 13, beginning at offset 12. The value 0x0800 identifies IPv4, whose header begins immediately after the 14-byte Ethernet header, at offset 14. Consequently, the IPv4 Protocol field is located at absolute frame offset 23. A manually constructed expression for identifying IPv4 TCP traffic is therefore:


'ether[12:2] = 0x0800 and ether[23] = 6'


Although this approach provides direct control over packet-header inspection, it introduces dependencies on the assumed link-layer encapsulation and header layout. For example, an 802.1Q VLAN tag introduces an additional four-byte header, shifting the encapsulated EtherType field from offset 12 to offset 16 and the beginning of the IPv4 header from offset 14 to offset 18. Consequently, hard-coded offsets based on an untagged Ethernet frame may fail to identify the intended fields when VLAN encapsulation is present. The vlan primitive in pcap-filter accommodates VLAN-tagged traffic by adjusting subsequent protocol offsets within the expression. Examiners should therefore establish the link-layer encapsulation and determine whether VLAN tags were preserved, removed by the network interface, or stripped by the capture mechanism during acquisition. This is particularly important when assessing capture completeness and interpreting the absence of expected packet characteristics.


Throughout this examination, particular attention will be given to the TCP control flags field, accessed using tcp[13]. Understanding its byte position and the application of bitwise masking provides the foundation for constructing more selective filters to identify TCP connection-establishment attempts, connection responses, resets, and other protocol behaviours.



BPF filter expressions can be constructed using two commonly employed mechanisms: protocol-relative byte-offset accessors and higher-level protocol primitives (or macros). Byte-offset notation, expressed as proto[offset:size], enables an examiner to inspect specific fields within a protocol header by specifying the protocol layer, zero-based byte offset, and number of bytes to retrieve. The supported access sizes are 1, 2, and 4 bytes, with 1 byte being the default. Protocol primitives, on the other hand, provide a more readable abstraction for expressing common packet-matching criteria, such as source and destination addresses, network ranges, transport protocols, and port numbers, without requiring the examiner to calculate individual header offsets.


The above figure demonstrates both approaches. Within an IPv4 header, the Protocol field is located at byte offset 9. Therefore, the expression ip[9] = 6 identifies IPv4 packets whose Protocol field indicates TCP. Similarly, the IPv4 source address occupies bytes 12 through 15 of the header. An examiner could match the address 192.168.2.2 using the four-byte expression ip[12:4] = 0xc0a80202. However, the equivalent protocol primitive, src host 192.168.2.2, provides a more readable and maintainable expression. The complete syntax, supported primitives, and packet-access operations are documented in the pcap-filter(7) manual page. Commonly used protocol primitives or macros include:


  • host <addr>: Matches packets in which the specified address appears as either the source or destination address.

  • src host <addr> / dst host <addr>: Restricts matching to packets with the specified source or destination address.

  • net <network>: Matches traffic originating from or destined for the specified network, for example, net 192.168.0.0/16.

  • src net <network> / dst net <network>: Restricts matching to the specified source or destination network.

  • port <n>: Matches packets with the specified source or destination TCP, UDP, or SCTP port.

  • src port <n> / dst port <n>: Matches packets using the specified source or destination transport port.

  • icmp: Identifies IPv4 ICMP traffic.

  • tcp: Identifies TCP traffic over IPv4 or IPv6.

  • udp: Identifies UDP traffic over IPv4 or IPv6.


An important consideration when constructing filters for fragmented traffic is the distinction between protocol identification and transport-header inspection. The expression ip proto 6 identifies IPv4 packets whose Protocol field specifies TCP, irrespective of their fragmentation status. Consequently, it can match both initial and non-initial fragments of a fragmented TCP datagram, even though non-initial fragments do not contain the TCP header. By contrast, protocol-relative accessors such as tcp[13] incorporate an implicit IPv4 fragment-offset check in libpcap's filter compilation process. This check ensures that the accessor references the TCP header in the initial fragment rather than interpreting arbitrary data in a subsequent fragment as TCP header fields. Specifically, the generated filter includes a condition equivalent to ip[6:2] & 0x1fff = 0. This permits unfragmented packets and initial fragments while excluding non-initial fragments, whose fragment offsets are non-zero.


This distinction is important in forensic investigations because a filter that identifies all IPv4 TCP fragments does not necessarily identify packets containing accessible TCP header fields. Examiners should therefore account for fragmentation when interpreting filtered results, particularly when investigating TCP flags, port numbers, and connection-establishment behaviour. For practical verification, the compiled filter instructions can be inspected using tcpdump -d. This allows an examiner to examine the generated BPF instructions and confirm whether the expected fragment-offset check is present. Throughout this article, the TCP control flags field, accessed through tcp[13], will be used extensively to demonstrate the construction and application of more selective packet filters.



BPF filter expressions can be constructed using two complementary mechanisms: byte-offset accessors and protocol primitives. Byte-offset notation provides direct access to packet-header fields, whereas protocol primitives offer higher-level abstractions for expressing common matching criteria, such as addresses, network ranges, transport protocols, and port numbers. These mechanisms can also be combined within a single expression to construct more selective packet-matching conditions.


Byte-offset notation follows the general form proto[offset:size] <relation> <value>. The protocol identifier establishes the reference point (offset zero), while the offset specifies the zero-based byte position of the field to be examined. The optional size parameter determines how many bytes are retrieved, defaulting to one byte. Supported access sizes are 1, 2, and 4 bytes, corresponding to byte, halfword, and word loads in classic BPF. Consequently, a three-byte field cannot be loaded in a single operation. It can be matched with a four-byte load and a mask, or with separate two-byte and one-byte comparisons. For example:


'ip[12:4] & 0xffffff00 = 0xc0a80200'


This expression compares the first three bytes of the IPv4 source address against 192.168.2, while disregarding the fourth byte. The split equivalent is ip[12:2] = 0xc0a8 and ip[14] = 2. Similarly, fields extending beyond four bytes, such as a six-byte Ethernet MAC address, require multiple comparisons. For example:


ether[6:4] = 0x00112233 and ether[10:2] = 0x4455


The two expressions collectively match the six-byte Ethernet source address 00:11:22:33:44:55. This approach allows an examiner to inspect fields that cannot be retrieved through a single supported BPF load operation. The relational operators supported by pcap-filter include =, !=, <, >, <=, and >=. Comparison values may be expressed using decimal or hexadecimal notation, and the arithmetic expression on the left-hand side may incorporate bitwise operations to isolate specific bits or field values. These capabilities are particularly useful when examining protocol flags, header attributes, and other fields that contain multiple independent bit-level indicators.


It is important to distinguish BPF capture-filter expressions from Wireshark display-filter expressions. BPF evaluates packet data using protocol-relative byte offsets and arithmetic operations, whereas Wireshark display filters operate on fields identified and dissected by the protocol-analysis engine. For example, tcp[2:2] = 80 accesses the TCP destination-port bytes directly, while tcp.dstport == 80 references the corresponding dissected field in Wireshark. Although both expressions can identify traffic associated with TCP destination port 80, their syntax and evaluation mechanisms are different. The following examples demonstrate how byte-offset expressions and protocol primitives can be applied during packet-level forensic examination:


1. IPv4 protocol identification

ip[9] = 0x01

This expression matches IPv4 packets whose Protocol field contains the value 1, identifying ICMPv4 traffic. Because the IPv4 Protocol field remains present in every IPv4 fragment, the expression can also match non-initial fragments. It therefore identifies the protocol associated with the original datagram without establishing that an ICMP header is present in every matching packet.


2. TCP destination-port examination

'tcp[2:2] != 80'

This expression selects IPv4 TCP packets whose destination-port field contains a value other than 80. The two-byte field at TCP header offsets 2 and 3 represents the destination port. The TCP byte-offset accessor incorporates an IPv4 fragment-offset check, so non-initial fragments that do not contain the TCP header are excluded.


3. UDP length-field examination

udp[4:2] > 0

This expression identifies IPv4 UDP datagrams whose UDP Length field contains a nonzero value. The field occupies bytes 4 and 5 of the UDP header and specifies the length of the UDP header and payload combined. A valid UDP datagram must have a declared length of at least eight bytes, corresponding to the minimum UDP header size. Consequently, the following expression can help identify UDP datagrams containing an invalid length value:


udp[4:2] < 8


This expression identifies IPv4 UDP datagrams whose Length field is below eight, the size of the UDP header alone. The field occupies bytes 4 and 5 of the UDP header and counts the header plus the payload, so any smaller value is invalid and may indicate corruption, a crafted packet, or an evasion attempt. The filter tests only the declared value. Confirming an inconsistency requires comparing it with the IP total length and the captured length.


4. ICMPv4 Destination Unreachable examination

'icmp[icmptype] = icmp-unreach and icmp[icmpcode] = 13'


This expression identifies ICMPv4 Destination Unreachable messages (Type 3) with Code 13, communication administratively prohibited, which a filtering device may return for a probe it blocks. Codes 9 and 10 are the network- and host-specific variants, Code 1 is host unreachable, and Code 3 is port unreachable, the usual response to a UDP probe of a closed port. The symbolic accessors icmptype and icmpcode are equivalent to icmp[0] and icmp[1]. No explicit fragment-offset test is needed: libpcap adds the IPv4 fragment check to icmp[...] accessors as it does for tcp[...], so non-initial fragments are excluded automatically. Compile the expression with tcpdump -d and look for the jset #0x1fff instruction to confirm. These messages may originate from the destination host or an intermediate device, and they quote the original IP header and at least the first eight bytes of the original payload, which allows the error to be associated with the probe that triggered it.


5. Transport-port identification

port 22

This protocol primitive matches IPv4 and IPv6 packets containing TCP, UDP, or SCTP source or destination port 22. Although port 22 is conventionally associated with SSH, the port number alone does not establish that the matching traffic represents SSH communication. Service identification requires additional examination of the application-layer exchange or corroborating evidence. For IPv4, the primitive requires the packet to be unfragmented or to contain the initial fragment. For IPv6, packets containing extension headers are subject to limitations in the port-matching implementation.


6. Destination-host identification

dst host 192.168.11.62


This expression matches packets in which 192.168.11.62 appears as the destination address within the applicable ARP, IPv4, or RARP address fields. Consequently, the expression is not restricted to IPv4 traffic alone. Where the investigative objective is specifically to identify IPv4 packets addressed to this host, the expression can be qualified as follows:


ip and dst host 192.168.11.62


This explicitly restricts the match to IPv4 traffic and excludes address matches originating from ARP or RARP packets.


7. Source-network identification

src net 192.0.0.0/8

This expression matches packets whose applicable source address belongs to the IPv4 network 192.0.0.0/8. The equivalent abbreviated notation is:


src net 192


pcap-filter infers the prefix length from the number of octets written: one octet implies /8, and 192.168 implies /16. The inference is positional rather than class-based, even though 192 falls in the former class C range. For forensic reporting and reproducibility, explicit CIDR notation is preferable because it makes the intended network boundary unambiguous.


These examples illustrate how BPF expressions can be used to isolate protocol-specific traffic, inspect individual header fields, and identify packet characteristics relevant to a network forensic investigation. Their correct application requires an understanding of protocol-header structures, fragmentation behaviour, address-family differences, and the limitations of the packet-capture environment.



Although BPF cannot directly address individual bits through its packet-data load operations, many protocol-header fields occupy only a portion of a byte. Such fields can be isolated using the bitwise AND operator (&) and an appropriate bitmask. A bitmask specifies which bits should be retained during the comparison, allowing an examiner to inspect individual flags, version identifiers, and other packed header attributes. It is important to distinguish the bitwise AND operator (&) from the Boolean and or && operators. The former performs a bit-level operation on numerical values, whereas the latter combines separate filtering conditions to determine whether both conditions must be satisfied. A practical example is the first byte of the IPv4 header, which contains two four-bit fields: the IP version in the upper nibble and the Internet Header Length (IHL) in the lower nibble.


'ip[0] & 0xf0 = 0x40'


This expression isolates the upper four bits and identifies IPv4 packets, for which the version field has a value of 4. The following expression examines the lower four bits:


'ip[0] & 0x0f > 5'


It identifies IPv4 packets whose IHL value exceeds five 32-bit words (20 bytes), indicating that the IPv4 header contains options beyond the standard 20-byte header.


Understanding byte offsets, load sizes, network byte order, and bitwise masking is fundamental to constructing precise BPF expressions. These techniques enable forensic examiners to move beyond basic address and port matching and inspect individual protocol-header attributes relevant to packet classification, anomaly identification, and network traffic reconstruction. When running BPF expressions from a shell, enclose the complete filter in single quotes. Characters such as &, |, <, >, ! and parentheses would otherwise be interpreted by the shell rather than passed to tcpdump. Every expression in this article is shown quoted for that reason.


Constructing BPF expressions becomes more challenging when the protocol field under investigation occupies only a portion of a byte. Consider an investigation in which the objective is to identify IPv4 packets containing header options. The Internet Header Length (IHL) field specifies the IPv4 header length in 32-bit words. Since each word represents four bytes, the minimum valid IPv4 header length of 20 bytes corresponds to an IHL value of 5. Therefore, an IHL value greater than 5 indicates that the header contains additional fields beyond the standard IPv4 header, including options.


The first byte of the IPv4 header contains two independent four-bit fields: the IP version in the upper nibble and the IHL in the lower nibble. Because BPF byte-offset notation retrieves the entire byte rather than an individual bit or nibble, an expression such as ip[0] returns both fields together. An initial filtering attempt might use the following expression:


'ip[0] > 0x45'


For a correctly formed IPv4 header, this comparison identifies packets with an IHL value greater than 5. This works because the upper nibble contains the IPv4 version value of 4, while the lower nibble contains the IHL value. Consequently, values ranging from 0x46 through 0x4f represent IPv4 headers with lengths greater than 20 bytes. However, this comparison evaluates the entire byte rather than isolating the IHL field. The upper nibble therefore influences the numerical comparison and can produce misleading results when examining malformed, anomalous, or deliberately crafted packets. For example, a byte value of 0x55 satisfies the condition ip[0] > 0x45, even though its lower nibble indicates an IHL value of 5, corresponding to a 20-byte header. Conversely, a value of 0x3f does not satisfy the comparison, despite its lower nibble indicating an IHL value of 15.


Although these examples represent invalid IPv4 version values, they illustrate an important forensic consideration: filtering should isolate the field relevant to the investigative question rather than rely on assumptions about the values of adjacent fields. To examine the IHL independently, apply a bitwise AND operation with the hexadecimal mask 0x0f. This retains the lower four bits and clears the upper four bits, allowing the IHL value to be evaluated without interference from the version field.


'ip[0] & 0x0f > 5'


This expression identifies packets whose IPv4 header-length field indicates a header longer than 20 bytes, irrespective of the value contained in the version nibble. For identifying valid IPv4 packets containing options, the expression can be further qualified:


'ip[0] & 0xf0 = 0x40 and ip[0] & 0x0f > 5'


The first condition identifies the IPv4 version, while the second identifies headers containing additional option bytes. Similarly, the following expression identifies IPv4 packets with an IHL value below the minimum permitted value of 5:


'ip[0] & 0x0f < 5'


Such packets contain an invalid IPv4 header-length declaration and may warrant further examination as malformed traffic, packet corruption, or deliberately crafted input. Since IHL is a four-bit field, its maximum representable value is 15, corresponding to a maximum IPv4 header length of 60 bytes. This example demonstrates the importance of bitwise masking in packet-level forensic examination. By isolating individual fields within a byte, investigators can construct more precise filters, identify anomalous protocol attributes, and avoid overlooking malformed traffic that would otherwise be excluded by overly simplistic numerical comparisons.



Examining individual TCP control flags requires the application of bitwise masking because each flag occupies a single bit within the TCP header. The eight conventional TCP control flags are located in the byte at offset 13, which can be accessed using either tcp[13] or the symbolic accessor tcp[tcpflags] in pcap-filter syntax. Each flag represents a distinct control indicator within the TCP segment and has an associated bit position and hexadecimal mask. From the most significant bit to the least significant bit, these are CWR (0x80), ECE (0x40), URG (0x20), ACK (0x10), PSH (0x08), RST (0x04), SYN (0x02), and FIN (0x01). The ninth flag, NS (Nonce Sum), is located separately in the least significant bit of the TCP header byte at offset 12.


These flags communicate control information associated with TCP operations, including connection establishment, acknowledgment, data delivery, and connection termination. Their interpretation must consider the surrounding packet sequence and the context of the TCP communication rather than treating an individual flag as conclusive evidence of a particular connection state. Three common forensic applications of TCP flag filtering include:


1. Identifying TCP connection attempts

'tcp[tcpflags] & (tcp-syn|tcp-ack) = tcp-syn'

This expression identifies TCP segments in which the SYN flag is set and the ACK flag is clear. Such segments are characteristic of initial TCP connection requests and are particularly useful when investigating connection attempts, port-scanning activity, and network reconnaissance.


2. Identifying SYN-ACK responses

'tcp[tcpflags] & (tcp-syn|tcp-ack) = (tcp-syn|tcp-ack)'

This expression identifies TCP segments in which both the SYN and ACK flags are set. In a conventional TCP three-way handshake, such segments represent responses to initial connection requests. They are particularly relevant when identifying ports that exhibit characteristics consistent with accepting TCP connection requests.


3. Identifying TCP reset segments

'tcp[tcpflags] & tcp-rst != 0'

This expression identifies TCP segments with the RST flag set. Reset segments are associated with connection rejection, abrupt connection termination, and other TCP error-handling conditions. During network investigations, an RST response may indicate that a probed port is closed, that an existing connection has been aborted, or that a network intermediary has generated or injected a reset segment. Its significance should therefore be established by examining the surrounding packet exchange, source and destination addresses, sequence information, and capture location.


When reconstructing TCP connection activity, the sequence of observed control flags is more informative than an isolated flag value. For example, a conventional TCP three-way handshake consists of an initial SYN segment from the initiating host, a SYN-ACK response from the responding endpoint, and a final ACK from the initiator. The receipt of the SYN-ACK indicates a positive response to the connection request, but the handshake is not considered complete until the final acknowledgment is exchanged. These filtering techniques provide a practical means of isolating TCP connection attempts, responses, and resets within larger packet captures. They also support the investigation of scanning activity, unsuccessful connection attempts, interrupted sessions, and potentially abnormal TCP communication patterns.


A bitmask is not always necessary when constructing BPF expressions. Where the investigative objective is to identify a packet whose entire target byte must contain an exact value, a direct equality comparison is sufficient. This approach is appropriate when the examiner knows which bits must be set and requires all remaining bits within that byte to be clear. The TCP control flags byte, located at offset 13 of the TCP header, contains eight conventional flags: CWR, ECE, URG, ACK, PSH, RST, SYN, and FIN. Each flag occupies a distinct bit position with an associated hexadecimal value: CWR (0x80), ECE (0x40), URG (0x20), ACK (0x10), PSH (0x08), RST (0x04), SYN (0x02), and FIN (0x01). The ninth flag, NS, is located separately in the least significant bit of byte 12. Equality is most useful when the signature is the entire byte, as with flag combinations that scanning tools emit and that legitimate stacks rarely or never send:


  1. 'tcp[13] = 0x02' — SYN-only: Matches segments with SYN set and all other byte-13 flags clear. This is the classic SYN-scan probe pattern, although legitimate connection attempts can produce the same combination. ECN-capable SYN packets may also set ECE and CWR, so this exact comparison excludes those packets.

  2. 'tcp[13] = 0x00' — NULL: Matches segments with none of the eight flags in byte 13 set. This pattern is unusual in normal TCP communication and may warrant investigation, but it is not conclusive evidence of a NULL scan.

  3. 'tcp[13] = 0x01' — FIN-only: Matches segments with FIN set and the other byte-13 flags clear. FIN is generally accompanied by ACK in established TCP connections, making FIN-only packets worth examining in the context of the surrounding traffic.

  4. 'tcp[13] = 0x29' — Xmas: Matches segments with FIN (0x01), PSH (0x08) and URG (0x20) set, while the other byte-13 flags are clear. This combination is commonly associated with Xmas scans.


These filters identify flag patterns, not scanning activity by themselves. In a forensic investigation, correlate matching packets with source and destination addresses, ports, packet timing, response patterns, retransmissions and the broader connection history before drawing conclusions.


Although exact-value comparisons are straightforward, they impose a strict matching condition. Every bit in the examined byte must correspond to the specified value. Consequently, a filter such as tcp[13] = 0x02 will exclude SYN segments carrying additional flags. For example, during ECN negotiation, an initial SYN segment may carry both ECE and CWR, producing a flags-byte value of 0xc2. Such a segment would not satisfy the exact comparison against 0x02. Where the investigative objective is to identify segments with SYN set and ACK clear, regardless of the state of the remaining conventional TCP flags, bitwise masking is more appropriate:


'tcp[13] & 0x12 = 0x02'


The mask 0x12 selects only the SYN and ACK bits. The comparison requires SYN to be set and ACK to be clear, while allowing the other six bits in byte 13 to assume either value. This distinction establishes an important principle in BPF filter construction: use direct equality comparisons when the complete byte value is significant, and use bitwise masking when only selected bits within that byte determine the matching condition.


The same principle applies to other protocol-header fields containing multiple subfields. For example, the first byte of the IPv4 header contains the four-bit version field in its upper nibble and the four-bit Internet Header Length (IHL) field in its lower nibble. For a conventional IPv4 header with a version value of 4 and an IHL value of 5, the complete byte is 0x45. An exact comparison can therefore identify this particular header configuration:


'ip[0] = 0x45'


This expression requires both the IPv4 version nibble and the IHL nibble to match their expected values. It identifies IPv4 headers with the standard 20-byte header length. However, where the investigative objective is to examine the IHL independently of the version field, a bitmask should be applied:


'ip[0] & 0x0f = 5'


The mask 0x0f retains the lower four bits containing the IHL value and discards the upper four bits containing the version. This allows the examiner to identify packets whose IHL field indicates five 32-bit words, irrespective of the value in the version field. Such filtering may be useful when investigating malformed or deliberately crafted packets. Similarly, to identify IPv4 headers whose declared length exceeds the standard 20-byte minimum, use:


'ip[0] & 0x0f > 5'


The masking operation is important because it isolates the IHL field before applying the numerical comparison. Without it, the upper version nibble would influence the comparison and could produce misleading results when examining malformed traffic. From a forensic perspective, the choice between exact-value comparison and bitwise masking should be determined by the investigative question. An exact comparison evaluates the complete byte, whereas a masked comparison isolates the specific bits relevant to the examination. Understanding this distinction helps prevent unintended exclusions and improves the precision and reproducibility of packet-filtering operations.


For our purposes, we categorize flag filters by how strictly they constrain the byte. Using SYN and ACK as an example:


  • Most exclusive (exact match). Every designated flag must be set, and no other flag bit in the byte may be set. This tests the whole byte against an exact value, so no mask is needed.
  • Less exclusive (all designated flags set, others allowed). Both SYN and ACK must be set, but any other flag bits may also be set. Mask the byte down to the bits of interest and require that all of them are set.
  • Least exclusive (at least one designated flag set, others allowed). SYN, ACK, or both must be set, and any other flag bits may also be set. Mask the byte and require that the result is nonzero.



To demonstrate the application of exact-value matching in packet-level forensic examination, consider the command shown in the above screenshot. The filter expression tcp[13] = 0x12 requires the entire TCP flags byte to equal the hexadecimal value 0x12. This value represents the combination of the SYN flag (0x02) and ACK flag (0x10), with all other conventional TCP flags in byte 13 clear. Because the expression evaluates the complete byte against a specific value, no bitmask is required. Any additional flag set within that byte changes its numerical value and causes the segment to be excluded from the matching results.


The resulting output contains TCP segments represented by tcpdump as Flags [S.], indicating that both SYN and ACK are set. In a conventional TCP three-way handshake, this flag combination characterizes the responding segment sent after an initial connection request. It therefore provides evidence of a positive response from the responding TCP endpoint or an intermediary acting on its behalf. In the displayed results, external hosts respond from TCP source port 443 to ephemeral destination ports on 192.168.99.51, including ports 51707 through 51712. The output also shows SYN-ACK exchanges involving two internal hosts communicating over TCP port 7680. These observations provide useful investigative leads for identifying responding endpoints, associated communication pairs, and services involved in the observed connection attempts.


The packet details also expose TCP options advertised by the responding endpoints, including Maximum Segment Size (MSS), window scaling, and SACK-permitted. These attributes provide additional information about the TCP configuration and capabilities of the responding systems. When examined collectively with option ordering, window characteristics, and other packet attributes, they can contribute to TCP stack fingerprinting and comparative analysis of network endpoints. However, they should not be treated as definitive operating-system identifiers in isolation. Two important limitations must be considered when interpreting these results.


  • First, the filter identifies only segments whose TCP flags byte exactly equals 0x12. It does not include the initiating SYN segments, which ordinarily carry the [S] flag combination, or the final ACK segments that complete the conventional TCP three-way handshake. Consequently, the filtered output provides visibility into the responding side of connection establishment rather than a complete reconstruction of the handshake. To establish whether individual connections were completed, correlate the SYN-ACK responses with their corresponding initial SYN and subsequent ACK segments, using the relevant network addresses, ports, sequence numbers, acknowledgment numbers, and packet timestamps where available.
  • Second, exact-value matching excludes SYN-ACK segments containing additional control flags. For example, an ECN-capable SYN-ACK may carry the ECE flag in addition to SYN and ACK, producing a flags-byte value of 0x52. Such a segment will not satisfy the exact comparison against 0x12, potentially resulting in an incomplete representation of the observed connection responses.


This example demonstrates the most restrictive form of TCP flag matching: exact-byte equality. It is appropriate when the investigative requirement is to identify a specific combination of flags with every other flag in the examined byte clear. Where additional flags may legitimately accompany the combination of interest, bitwise masking provides a more inclusive and analytically appropriate alternative.


The second category of BPF filtering uses bitwise masking to identify packets containing specific flag combinations without requiring the remaining bits in the examined byte to have particular values. Consider an investigation in which the objective is to identify every TCP segment with both the SYN and ACK flags set, irrespective of whether additional control flags are present. Unlike exact-value matching, this requirement cannot be satisfied reliably by comparing the entire TCP flags byte against a single hexadecimal value because other flag bits may vary between matching segments. Instead, a bitmask is applied to isolate the two flags relevant to the investigation. The following expression accomplishes this:


'tcp[13] & 0x12 = 0x12'


The expression performs a bitwise AND operation between the TCP flags byte and the hexadecimal mask 0x12. This mask combines the ACK bit (0x10) and SYN bit (0x02). During the operation, every bit corresponding to a zero in the mask is cleared, while the selected SYN and ACK bits retain their original values. The resulting value is then compared against 0x12. For the comparison to succeed, both SYN and ACK must be set in the original TCP flags byte. The remaining six flags are not constrained by the comparison and may be either set or clear. This approach effectively applies exact-value matching to the two bits of investigative interest while disregarding unrelated flag combinations. To examine this behaviour against the 24-hour packet capture, execute the following command:



The -r option specifies the existing capture file, -nn disables both hostname and service-port resolution, and -tttt displays packet timestamps using a date-and-time format. The filter selects IPv4 TCP segments containing both SYN and ACK flags, regardless of the state of the other conventional TCP flags. The resulting output contains SYN-ACK segments represented by tcpdump using the notation Flags [S.]. Each matching packet is accompanied by its capture timestamp, allowing the examiner to establish when the responses were observed. In the displayed results, the selected segments occur on 10 December 2023, beginning at 21:13:23.


These observations provide evidence of TCP connection responses from the identified endpoints. The accompanying TCP options, including Maximum Segment Size (MSS), window scaling, and SACK-permitted, provide additional information about the advertised TCP capabilities of the responding systems. When examined alongside option ordering, window characteristics, and other packet attributes, these details can contribute to comparative TCP stack fingerprinting.


An important observation from the displayed output is that all visible matching segments carry SYN and ACK without additional flags. Consequently, their flag combinations are identical to those selected by the more restrictive exact-value expression 'tcp[13] = 0x12'. The visible output alone therefore does not demonstrate the additional coverage provided by bitwise masking. To establish whether the broader expression identifies additional packets, compare the number of matching packet records returned by both filters across the complete capture:


Exact-value comparison:

tcpdump -nn -r timeserversync_24hr.pcap 'tcp[13] = 0x12' | wc -l


Bitwise-mask comparison:

tcpdump -nn -r timeserversync_24hr.pcap 'tcp[13] & 0x12 = 0x12' | wc -l



On this capture, the exact-value filter returned N packets and the masked filter returned M. [If equal: No SYN-ACK here carried additional flags, so the expressions are equivalent on this capture, but only the masked form remains correct if ECN negotiation appears in other captures.] [If M > N: The M − N additional packets carry SYN and ACK together with another flag, for example ECE (flags byte 0x52).]


A higher count for the masked expression would indicate that additional packets contain both SYN and ACK flags but also have one or more other conventional TCP flags set. For example, an ECN-capable SYN-ACK carrying the ECE flag has a flags-byte value of 0x52 and would satisfy the masked expression but not the exact-value comparison.


For forensic interpretation, these counts represent matching packet records rather than unique TCP connections. Retransmissions, duplicate observations, and repeated connection attempts can produce multiple matching packets associated with the same communication. Accordingly, packet-count differences should be followed by flow correlation and examination of the relevant TCP sequence and acknowledgment numbers before drawing conclusions about the number of distinct connection attempts.


This example demonstrates a less restrictive form of TCP flag filtering. Unlike exact-byte comparison, bitwise masking allows an examiner to identify packets containing the required control flags while accommodating additional flags that may legitimately accompany them. This makes masking particularly useful when investigating TCP connection establishment, ECN negotiation, and other network behaviours in which multiple control flags may coexist.


The least restrictive category of bitwise filtering identifies a TCP segment whenever at least one of the designated control flags is set, while allowing all remaining flags to assume either value. Unlike the previous example, which required both SYN and ACK to be present, this approach uses a nonzero comparison to identify packets containing either flag or both simultaneously. Consider the following BPF expression:


'tcp[13] & 0x12 != 0'


The hexadecimal mask 0x12 combines the ACK flag (0x10) and SYN flag (0x02). The bitwise AND operation isolates these two bits from the TCP flags byte, effectively clearing the remaining six flag positions before the comparison is evaluated. Three nonzero outcomes are possible:


  • 0x10: ACK is set, while SYN is clear.

  • 0x02: SYN is set, while ACK is clear.

  • 0x12: Both ACK and SYN are set.


The only outcome that evaluates to zero is 0x00, indicating that neither designated flag is present. Consequently, the comparison != 0 selects every TCP segment containing SYN, ACK, or both, irrespective of the state of the remaining conventional TCP flags. To examine this behaviour against the 24-hour packet capture, execute:



The resulting output illustrates the broader coverage of this filtering approach. Unlike the previous expressions, which concentrated on SYN-ACK responses, this filter also identifies ordinary TCP acknowledgment segments and other communication carrying the ACK flag. In the displayed results, the matching segments include:


  • [R.] — RST and ACK: A reset segment observed from 204.79.197.200:443 at 21:13:00. This indicates that the segment carries both reset and acknowledgment flags.

  • [.] — ACK: An acknowledgment segment without additional conventional flags set.

  • [P.] — PSH and ACK: A segment carrying both push and acknowledgment flags, commonly encountered during TCP data transfer.


The subsequent visible packets show communication between 192.168.99.51:52074 and 167.71.97.235:9200. The exchange includes TCP data segments with reported payload lengths of 4,125, 4,125, 1,460, and 5,107 bytes, alongside acknowledgment traffic from the remote endpoint. These observations demonstrate why the filter is considerably more inclusive than the previous two categories. ACK is commonly set on TCP segments exchanged after connection establishment, including acknowledgment-only packets and data-bearing segments. Consequently, a filter requiring ACK or SYN can return substantial portions of established TCP communication rather than exclusively identifying connection-establishment activity.


An important observation in the displayed output is that none of the visible matching segments is an initial SYN without ACK. Instead, the selected packets match because ACK is set. This illustrates how a nonzero comparison can capture a broad range of TCP communication, depending on the flags selected in the mask. The mask determines whether a nonzero test is useful. A mask containing ACK returns most of an established session, because ACK is set on nearly every segment after the handshake. A mask built from flags that occur only at specific moments is far more selective. Using 0x07, which combines FIN (0x01), SYN (0x02) and RST (0x04):


'tcp[13] & 0x07 != 0'


This selects segments that open, close, or abort a connection, reducing a day-long capture to its connection-state transitions. The matches are still packet records, not connections, so retransmitted SYNs and the FIN-ACKs from both directions must be correlated by address and port pair.


This is the least restrictive of the three comparisons, and it is only as selective as the flags in the mask. Choose the mask by the connection event under investigation, not by the flags that happen to be common. By combining a bitmask with a nonzero comparison, an examiner can identify packets containing any of the designated control flags without imposing conditions on the remaining bits. The resulting coverage is useful for broad traffic triage, but the matching packets must subsequently be classified and correlated to distinguish connection establishment, data transfer, acknowledgment activity, and connection termination.



Another useful application of bitwise masking is identifying TCP segments that contain header options. The TCP header includes a four-bit field called the Data Offset, which specifies the length of the TCP header in 32-bit words. Similar to the Internet Header Length (IHL) field in IPv4, the Data Offset value must be multiplied by four to obtain the header length in bytes. A standard TCP header without options occupies 20 bytes, corresponding to a Data Offset value of 5. When TCP options are present, the header length increases accordingly, up to a maximum of 60 bytes, represented by a Data Offset value of 15. The Data Offset field occupies the upper four bits (high-order nibble) of byte 12 in the TCP header. The lower four bits contain reserved bits and the NS flag. Because these fields share the same byte, examining the Data Offset independently requires a bitwise mask to eliminate the lower nibble. The appropriate mask is 0xf0, which retains the upper four bits and clears the lower four bits:


tcp[12] & 0xf0


An important distinction arises when interpreting the resulting value. Because the Data Offset occupies the upper nibble, its numerical contribution is multiplied by 16. Consequently, a Data Offset value of 5 produces 0x50, equivalent to 80 in decimal notation. This value does not represent an 80-byte TCP header; it represents the encoded value of 5 in the upper nibble. The following expressions demonstrate how to use this field to classify TCP headers:


1. Identify TCP segments containing options

'tcp[12] & 0xf0 > 0x50'

This expression identifies TCP segments whose Data Offset exceeds 5, indicating a header longer than the standard 20-byte minimum and therefore the presence of TCP options.


2. Identify TCP segments without options

'tcp[12] & 0xf0 = 0x50'

This expression selects TCP segments with a Data Offset value of exactly 5, corresponding to a 20-byte TCP header without options.


3. Interpret the header length in bytes

The masked Data Offset value can also be shifted right by two bit positions to convert its encoded representation into the corresponding header length in bytes:


'(tcp[12] & 0xf0) >> 2 > 20'


This expression identifies TCP headers longer than 20 bytes. The right shift effectively divides the masked value by four, converting the encoded upper-nibble representation into the actual header length in bytes.


From a forensic perspective, TCP options provide useful information about the capabilities and configuration advertised by communicating endpoints. Options such as Maximum Segment Size (MSS), window scaling, Selective Acknowledgment (SACK), and timestamps can contribute to TCP stack fingerprinting and comparative analysis of network communications. These options are particularly common in SYN and SYN-ACK segments exchanged during connection establishment. However, they may also occur in segments belonging to established TCP sessions. Consequently, filtering an extensive packet capture for TCP headers containing options can produce a substantial result set, particularly in captures containing large volumes of active TCP communication.


The presence of TCP options should therefore be treated as an analytical attribute rather than an anomaly by itself. Their values, ordering, combinations, and occurrence across different communication flows provide additional context when reconstructing network activity and examining the characteristics of communicating systems.



A TCP Data Offset value occupies the high-order nibble of byte 12 in the TCP header. A comparison against tcp[12] evaluates the entire byte, not just the bits belonging to the Data Offset field. For example, a Data Offset of 5 is encoded as 0x50 (80 decimal), so tcp[12] > 5 does not test whether the Data Offset exceeds 5; it matches virtually every valid TCP header. To select TCP segments with a Data Offset greater than 5, indicating a TCP header longer than 20 bytes, you can use any of the following equivalent expressions:


  • Mask and compare in place: Clear the low-order nibble with 0xf0, then compare the remaining value with 0x50, the encoded representation of a Data Offset of 5:
    ' (tcp[12] & 0xf0) > 0x50'

  • Divide by 16: Integer division by 16 discards the low-order four bits and leaves the Data Offset value, expressed in 32-bit words:
    'tcp[12] / 16 > 5'

  • Shift right by 4: A right shift of four bits has the same effect as integer division by 16:
    'tcp[12] >> 4 > 5'


The division and shift expressions do not require an explicit mask because both discard the low-order nibble, which contains the reserved bits and the NS flag. Masking retains the Data Offset in its original bit position; division and shifting extract it as a value in words. If you prefer to express the comparison in bytes, mask the low-order nibble and shift the result right by two bits. This converts the encoded high-nibble value into the TCP header length in bytes:


'(tcp[12] & 0xf0) >> 2 > 20'


Masking first is important here: shifting the unmasked byte could allow bits from the reserved and NS fields to affect the result.



Option 1 uses the same masking technique as before, but now the field of interest is the high-order nibble. The Data Offset occupies the high four bits of byte 12. The low four bits hold the three reserved bits and the NS flag, which may be nonzero, so they must not influence the comparison.


A bitwise AND with the mask 0xf0 (1111 0000) preserves the high nibble and forces the low nibble to zero. In the diagram, the byte 0101 ???? ANDed with 1111 0000 becomes 0101 0000, which is 0x50. The result stays in the high nibble, so the comparison value must be written in that position as well: Data Offset 5 is 0x50, and Data Offset 6 is 0x60. The filter 'tcp[12] & 0xf0 > 0x50' therefore selects segments whose Data Offset is greater than 5, meaning the header is longer than 20 bytes and carries options. The byte in the diagram (Data Offset 5, masked to 0x50) is exactly the boundary and does not match. A byte beginning 0110 masks to 0x60, which is greater than 0x50, so it does. The mask matters even when the reserved bits should be zero. Without it, tcp[12] > 0x50 would also match a header with Data Offset 5 and NS set (0x51).



Option 2 treats the high-order nibble as if it were in the low-order position, so you compare against the field’s natural value. The Data Offset sits in the 16¹ position of the byte, so the byte is 16 times the field value. Dividing by 16 undoes that. BPF arithmetic is unsigned integer arithmetic, so the division truncates and the low nibble (the reserved bits and NS) disappears without a mask. The bits 0101 ???? divide to 5 whatever the ? bits are, so the byte can range from 0x50 to 0x5f and the result is still 5.


'tcp[12] / 16 > 5'


The same effect comes from a right shift. Shifting right by n bits divides an unsigned value by 2ⁿ and discards the n low bits, and 16 is 2⁴, so a 4-bit shift discards the low nibble and leaves 0000 0101:


'tcp[12] >> 4 > 5'


Both forms select segments whose Data Offset exceeds 5, meaning headers longer than 20 bytes. Because they return the field in words, the comparison constant is 5. This differs from Option 1, which keeps the value in the high nibble and compares against 0x50. pcap-filter supports + - * / & | << >> (newer libpcap versions add % and ^), so confirm your build before relying on the less common operators. Precedence differs from C. The bitwise operators bind more tightly than the comparison operators, so tcp[13] & 0x12 = 0x12 parses as (tcp[13] & 0x12) = 0x12, but + and - bind more tightly than <<, >> and &, so parenthesise any mixed arithmetic.BPF’s efficiency comes from being a small bytecode program that reads only the bytes it names. On a live capture, the compiled program runs in the kernel, so non-matching packets are never copied to userspace. When reading a saved capture with -r, the same filter runs in userspace, so the benefit is precision and reproducibility rather than speed. A filter is a hypothesis about which bits matter. Use equality when the whole byte is the signature, a mask when only some bits are, and record the expression, tool version, and output hash so that the result can be reproduced.



Post a Comment

Previous Post Next Post