← Chapter 61 Index | Part 2 of 3
What Is a TCP Segment?
TCP is a stream protocol — from the application’s view, it looks like a continuous pipe of bytes. But underneath, TCP breaks that stream into chunks called segments before handing them to IP. Each segment has a header (control information) followed by optional data (the actual payload bytes).
Understanding the segment format is critical because every TCP behaviour — connection setup, data transfer, error handling, flow control, congestion control — is controlled by fields in this header. When you use Wireshark or an Ellisys analyser, these are the exact bytes you see.
TCP Header Layout
The TCP header is at minimum 20 bytes. With options, it can be up to 60 bytes.
| Bit 0 | Bit 15 | Bit 16 | Bit 31 | |||
| Source Port (16 bits) | Destination Port (16 bits) | ||||
| Sequence Number (32 bits) | |||||
| Acknowledgement Number (32 bits) | |||||
| Hdr Len (4 bits) |
Reserved (4 bits) |
Control Bits (8 bits: CWR ECE URG ACK PSH RST SYN FIN) |
Window Size (16 bits) | ||
| TCP Checksum (16 bits) | Urgent Pointer (16 bits) | ||||
| Options (0 – 40 bytes, variable) | |||||
| Data (payload — 0 or more bytes) | |||||
Field-by-Field Breakdown
The port number of the sender. For a client, this is typically an ephemeral port (automatically assigned by the OS, usually in the range 32768–60999 on Linux). For a server response, this would be a well-known port like 80 (HTTP) or 443 (HTTPS).
The port number of the receiver. When a client connects to a web server, the destination port is 80 or 443. Together with the source port and IP addresses, the four-tuple (src IP, src port, dst IP, dst port) uniquely identifies a TCP connection.
This field tells the receiver the position of the first byte of data in this segment within the overall byte stream. TCP assigns every byte a unique sequence number.
For example, if this segment carries bytes 1001 to 1500, the sequence number is 1001. During the initial SYN, the sequence number is the random Initial Sequence Number (ISN) chosen by the sender — not yet data, but the starting point.
This field is only meaningful when the ACK control bit is set. It contains the sequence number of the next byte the receiver expects. It is also the “up to here I have received everything” guarantee. If the ACK number is 1501, it means “I got everything up to byte 1500, now send me starting from 1501.”
Counts the header length in units of 32-bit (4-byte) words. With 4 bits you can represent 0 to 15, meaning the max header length is 15 × 4 = 60 bytes. The minimum is 5 × 4 = 20 bytes (no options). This field lets the receiving TCP know where the data starts, since the options field varies in size.
These 4 bits are reserved for future use and must be set to zero. Any packet with non-zero reserved bits was historically dropped, though modern TCP implementations are more lenient.
7. Control Bits (Flags) — 8 bits
This is the most important part of the header. Each bit is an independent flag that changes the meaning of the segment. Multiple flags can be set at the same time.
| Flag | Full Name | What It Means |
|---|---|---|
CWR |
Congestion Window Reduced | Sender has reduced its sending rate in response to a congestion signal. Part of the ECN algorithm. |
ECE |
ECN Echo | Receiver is echoing back a congestion signal from the network (ECN-capable). Requires kernel support, enabled via /proc/sys/net/ipv4/tcp_ecn. |
URG |
Urgent | When set, the Urgent Pointer field is valid. Signals that part of the data should be prioritised. Rarely used in modern applications. |
ACK |
Acknowledgement | The Acknowledgement Number field is valid. This bit is set in almost every segment after the initial SYN. |
PSH |
Push | Tells the receiving TCP to deliver this data to the application immediately without waiting to fill a buffer. Useful for interactive applications like telnet. |
RST |
Reset | Abruptly terminates the connection. Sent when an error occurs, or when a packet arrives for a port that has no listener. |
SYN |
Synchronise | Used during connection establishment (the 3-way handshake). Contains the initial sequence number for this direction. |
FIN |
Finish | The sender has no more data to send. Used to gracefully close one direction of the connection. |
Common Flag Combinations
| Flags Set | When It Appears |
|---|---|
SYN |
First packet from client during 3-way handshake |
SYN + ACK |
Server’s reply during 3-way handshake (acknowledges client SYN, sends its own SYN) |
ACK |
All normal data and acknowledgement segments after connection is established |
FIN + ACK |
Closing a connection — “I am done sending, and I acknowledge your last data” |
RST + ACK |
Aborting a connection abruptly — seen when connecting to a closed port |
This field implements flow control. When a receiver sends an ACK, it also tells the sender how many more bytes it can accept right now (the size of its receive buffer). The sender must not send more than this many bytes beyond the last acknowledged byte.
A window size of 0 means “stop sending — I am full”. This is called a zero window condition. TCP Window Scale options can extend this beyond 65535 bytes for high-speed links.
A 16-bit error-detection code covering the TCP header, TCP data, and a special 12-byte structure called the TCP pseudoheader.
The pseudoheader is not transmitted — it is constructed in memory for checksum calculation only. It contains:
| Source IP Address | 4 bytes |
| Destination IP Address | 4 bytes |
| Zero byte + Protocol (value 6 for TCP) | 2 bytes |
| TCP Segment Length | 2 bytes |
Why include IP addresses in a TCP checksum? To catch misdelivered packets — if IP routes a packet to the wrong host, the checksum will fail because the destination IP in the pseudoheader will not match what was expected. UDP uses an identical pseudoheader scheme.
Only meaningful when the URG flag is set. Points to the last byte of “urgent data” in the segment — data that should be delivered out-of-band to the application as fast as possible, bypassing normal buffering. In practice, URG / urgent data is rarely used in modern software and is considered a legacy feature.
Variable-length field for negotiating optional features. Common options include:
- MSS (Maximum Segment Size) — each side declares the largest segment it will accept. Exchanged during SYN.
- Window Scale — extends the 16-bit window size field by a scale factor, enabling larger windows for fast links.
- SACK (Selective ACK) — allows the receiver to acknowledge non-contiguous blocks, reducing retransmission.
- Timestamps — used to calculate round-trip time and protect against old duplicate packets (PAWS).
Reading TCP Flags in Raw Sockets (C Code)
In a raw socket or packet capture, you can read the TCP flags directly from the header struct. This is useful for tools like a simple port scanner or packet analyser.
#include <stdio.h>
#include <stdint.h>
#include <netinet/tcp.h> /* struct tcphdr */
#include <netinet/ip.h> /* struct iphdr */
#include <sys/socket.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <string.h>
/* Pretty-print the TCP flags from a tcphdr */
void print_tcp_flags(const struct tcphdr *tcp)
{
printf("TCP Flags: ");
if (tcp->syn) printf("SYN ");
if (tcp->ack) printf("ACK ");
if (tcp->fin) printf("FIN ");
if (tcp->rst) printf("RST ");
if (tcp->psh) printf("PSH ");
if (tcp->urg) printf("URG ");
printf("\n");
}
/* Print a summary of a TCP segment */
void print_tcp_segment(const struct tcphdr *tcp)
{
printf("Src Port : %u\n", ntohs(tcp->source));
printf("Dst Port : %u\n", ntohs(tcp->dest));
printf("Seq # : %u\n", ntohl(tcp->seq));
printf("Ack # : %u\n", ntohl(tcp->ack_seq));
printf("Header Len: %u bytes\n", tcp->doff * 4);
printf("Window : %u\n", ntohs(tcp->window));
printf("Checksum : 0x%04x\n", ntohs(tcp->check));
print_tcp_flags(tcp);
}
int main(void)
{
/* Raw socket to capture all IP packets on this machine */
int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
if (sockfd == -1) {
perror("socket (need root)");
return 1;
}
unsigned char buffer[65536];
struct iphdr *ip;
struct tcphdr *tcp;
printf("Listening for TCP segments (Ctrl-C to stop)...\n\n");
while (1) {
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n < 0) break;
ip = (struct iphdr *)(buffer + 14); /* skip Ethernet header */
/* Only process TCP packets */
if (ip->protocol != 6) continue;
int ip_hlen = ip->ihl * 4;
tcp = (struct tcphdr *)((unsigned char *)ip + ip_hlen);
printf("--- TCP Segment ---\n");
printf("IP: %s ", inet_ntoa(*(struct in_addr *)&ip->saddr));
printf("→ %s\n", inet_ntoa(*(struct in_addr *)&ip->daddr));
print_tcp_segment(tcp);
printf("\n");
}
return 0;
}
Compile and run (requires root):
gcc -o tcp_sniff tcp_sniff.c
sudo ./tcp_sniff
ECN: Explicit Congestion Notification
CWR and ECE are used together as part of ECN (RFC 3168). ECN is a way to signal congestion without dropping packets. Normally, when a router is congested, it simply drops packets. With ECN, the router can mark packets instead, and the receiver echoes this back to the sender using the ECE flag. The sender then reduces its rate and confirms with CWR.
Check and enable ECN on Linux:
# Check current setting (0 = disabled, 1 = enabled, 2 = enabled for outgoing only)
cat /proc/sys/net/ipv4/tcp_ecn
# Enable ECN
echo 1 | sudo tee /proc/sys/net/ipv4/tcp_ecn
Interview Questions and Answers
/proc/sys/net/ipv4/tcp_ecn.