Loading learning content...
When a destination host receives fragments of an IP datagram, it faces a critical challenge: How does it know where each fragment belongs in the original datagram? The fragments may arrive out of order, from different paths, at different times. The answer lies in the Fragment Offset field—a 13-bit value that precisely locates each fragment's data within the original payload.
Understanding offset calculations is essential for:
This page transforms the Fragment Offset field from an abstract header value into a practical tool for network analysis.
By the end of this page, you will understand: (1) How the Fragment Offset field encodes position in the original datagram, (2) The 8-byte unit conversion between field value and actual byte position, (3) How to calculate offsets for each fragment in a sequence, (4) Verification techniques to ensure correct fragmentation, and (5) Common offset-related errors and how to detect them.
The Fragment Offset is a 13-bit field in the IPv4 header that specifies where this fragment's data belongs in the original, unfragmented datagram's payload. It answers the fundamental question: "At what byte position does this fragment's data start in the original message?"
Location in IPv4 Header:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL | TOS/DSCP | Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
↑ ↑
3 bits 13 bits (0-8191)
(R/DF/MF)
Key Properties:
| Property | Value/Description |
|---|---|
| Field size | 13 bits |
| Value range | 0 to 8191 |
| Unit | 8-byte blocks (octets) |
| Actual byte range | 0 to 65,528 (8191 × 8) |
| First fragment offset | Always 0 |
| Position in word | Bits 3-15 of 32-bit word at offset 4 |
Byte Position = Fragment Offset Field Value × 8
A Fragment Offset of 185 means the fragment's data starts at byte position 185 × 8 = 1,480 in the original datagram. Conversely, to set the Fragment Offset for data starting at byte 2,960, calculate 2960 ÷ 8 = 370.
Why 8-Byte Units?
With only 13 bits available, the maximum value is 8,191. If this represented individual bytes, we could only address the first 8,191 bytes of a datagram—far short of the 65,535-byte maximum IP datagram size. By using 8-byte units:
When fragmenting a datagram, the offset for each fragment is determined by the cumulative size of all preceding fragments' data. Here's the systematic approach:
The Offset Calculation Formula:
Fragment N Offset (field value) = (Sum of data in fragments 1 to N-1) ÷ 8
Fragment N Offset (bytes) = Sum of data in fragments 1 to N-1
Step-by-Step Process:
Complete Example:
Original datagram: 5,000 bytes data (20-byte header, 5,020 bytes total) MTU: 1,500 bytes Maximum fragment data: 1,480 bytes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
# Complete offset calculation for a 5000-byte datagramoriginal_data = 5000max_fragment_data = 1480 # MTU 1500 - 20 byte header, 8-byte aligned # Calculate fragments and their offsetsfragments = []current_offset_bytes = 0remaining_data = original_data while remaining_data > 0: # Determine this fragment's data size if remaining_data > max_fragment_data: fragment_data = max_fragment_data more_fragments = 1 # MF = 1 else: fragment_data = remaining_data more_fragments = 0 # MF = 0 (last fragment) # Calculate offset field value (in 8-byte units) offset_field = current_offset_bytes // 8 fragments.append({ 'data_size': fragment_data, 'offset_bytes': current_offset_bytes, 'offset_field': offset_field, 'mf': more_fragments }) # Move to next fragment current_offset_bytes += fragment_data remaining_data -= fragment_data # Display resultsprint(f"Original datagram: {original_data} bytes data\n")print("Fragment Details:")print("-" * 70)for i, f in enumerate(fragments): total_length = f['data_size'] + 20 # Fragment total = data + IP header print(f"Fragment {i+1}: Data={f['data_size']:4d} bytes, " f"Offset={f['offset_field']:4d} ({f['offset_bytes']:5d} bytes), " f"MF={f['mf']}, Total={total_length} bytes") # Verificationtotal_data = sum(f['data_size'] for f in fragments)print(f"\nVerification: Total data = {total_data} bytes ({'✓' if total_data == original_data else '✗'})") # Output:# Original datagram: 5000 bytes data## Fragment Details:# ----------------------------------------------------------------------# Fragment 1: Data=1480 bytes, Offset= 0 ( 0 bytes), MF=1, Total=1500 bytes# Fragment 2: Data=1480 bytes, Offset= 185 ( 1480 bytes), MF=1, Total=1500 bytes# Fragment 3: Data=1480 bytes, Offset= 370 ( 2960 bytes), MF=1, Total=1500 bytes# Fragment 4: Data= 560 bytes, Offset= 555 ( 4440 bytes), MF=0, Total=580 bytes## Verification: Total data = 5000 bytes ✓The Fragment Offset refers to the position of DATA in the original datagram, not the position of the fragment in the transmitted byte stream. Each fragment's IP header is NOT counted in offset calculations—only the payload data matters.
Visualizing how fragments map to the original datagram clarifies offset semantics and aids in troubleshooting.
Linear Mapping Visualization:
ORIGINAL DATAGRAM (5000 bytes data):
┌───────────────────────────────────────────────────────────────────┐
│ Data bytes: 0 ... 1479 | 1480 ... 2959 | 2960 ... 4439 | 4440 ... 4999
└───────────────────────────────────────────────────────────────────┘
↓ Fragmentation (MTU 1500) ↓
FRAGMENTS:
┌──────────────────┐ Fragment 1: Offset=0, Size=1480, MF=1
│ HDR | Data 0-1479│ Covers bytes 0 to 1479
└──────────────────┘
┌──────────────────┐ Fragment 2: Offset=185, Size=1480, MF=1
│ HDR | 1480-2959 │ Offset 185 × 8 = 1480, covers bytes 1480 to 2959
└──────────────────┘
┌──────────────────┐ Fragment 3: Offset=370, Size=1480, MF=1
│ HDR | 2960-4439 │ Offset 370 × 8 = 2960, covers bytes 2960 to 4439
└──────────────────┘
┌─────────────┐ Fragment 4: Offset=555, Size=560, MF=0
│ HDR |4440..│ Offset 555 × 8 = 4440, covers bytes 4440 to 4999
└─────────────┘
Byte Position Coverage Verification:
For correct reassembly, fragments must cover every byte position exactly once:
| Fragment | Offset (field) | Offset (bytes) | Size | Covers Bytes | End Byte |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 1480 | 0-1479 | 1479 |
| 2 | 185 | 1480 | 1480 | 1480-2959 | 2959 |
| 3 | 370 | 2960 | 1480 | 2960-4439 | 4439 |
| 4 | 555 | 4440 | 560 | 4440-4999 | 4999 |
Verification checks:
Gaps occur when the next fragment's offset × 8 is greater than previous offset × 8 + previous size. Overlaps occur when the next fragment's offset × 8 is less than previous offset × 8 + previous size. Both indicate fragmentation errors or malicious packets (overlap attacks).
Given a fragment, we need to determine exactly which bytes of the original datagram it carries. This is essential for reassembly logic and packet analysis.
Position Reconstruction Formulas:
First byte position = Fragment Offset × 8
Last byte position = (Fragment Offset × 8) + Data Size - 1
Data Size Extraction:
The fragment's data size isn't stored directly—it must be calculated:
Fragment Data Size = Total Length - (IHL × 4)
Where:
- Total Length = Value in IPv4 Total Length field
- IHL = Internet Header Length (in 32-bit words)
- IHL × 4 = Header size in bytes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
def reconstruct_fragment_position(total_length, ihl, fragment_offset, mf): """ Given fragment header values, determine the byte range covered. Args: total_length: IPv4 Total Length field value ihl: Internet Header Length field value (in 32-bit words) fragment_offset: Fragment Offset field value (in 8-byte units) mf: More Fragments flag (1 = more fragments follow, 0 = last) Returns: tuple: (first_byte, last_byte, data_size, is_last) """ # Calculate header size and data size header_size = ihl * 4 data_size = total_length - header_size # Calculate byte positions first_byte = fragment_offset * 8 last_byte = first_byte + data_size - 1 return { 'first_byte': first_byte, 'last_byte': last_byte, 'data_size': data_size, 'is_last': mf == 0, 'header_size': header_size } # Example: Analyze fragments from a packet capturefragments = [ {'total_length': 1500, 'ihl': 5, 'offset': 0, 'mf': 1}, {'total_length': 1500, 'ihl': 5, 'offset': 185, 'mf': 1}, {'total_length': 1500, 'ihl': 5, 'offset': 370, 'mf': 1}, {'total_length': 580, 'ihl': 5, 'offset': 555, 'mf': 0},] print("Fragment Analysis:")print("-" * 65)for i, frag in enumerate(fragments): result = reconstruct_fragment_position( frag['total_length'], frag['ihl'], frag['offset'], frag['mf'] ) print(f"Fragment {i+1}: Bytes {result['first_byte']:5d} - {result['last_byte']:5d} " f"({result['data_size']:4d} bytes) {'[LAST]' if result['is_last'] else ''}") # Calculate total original sizelast_frag = fragments[-1]last_result = reconstruct_fragment_position( last_frag['total_length'], last_frag['ihl'], last_frag['offset'], last_frag['mf'])original_size = last_result['last_byte'] + 1print(f"\nOriginal datagram data size: {original_size} bytes") # Output:# Fragment Analysis:# -----------------------------------------------------------------# Fragment 1: Bytes 0 - 1479 (1480 bytes) # Fragment 2: Bytes 1480 - 2959 (1480 bytes) # Fragment 3: Bytes 2960 - 4439 (1480 bytes) # Fragment 4: Bytes 4440 - 4999 ( 560 bytes) [LAST]## Original datagram data size: 5000 bytesThe original datagram's total data size = Last fragment's (Offset × 8) + Last fragment's data size. Identify the last fragment by MF=0. Add 20+ bytes for the original header to get the complete original datagram size.
A datagram may be fragmented multiple times as it traverses networks with decreasing MTUs. Understanding how offsets behave during re-fragmentation is crucial.
The Re-Fragmentation Principle:
When a router fragments an already-fragmented packet, the new fragments' offsets are calculated relative to the original datagram, not relative to the intermediate fragment.
Scenario: Multi-Hop Fragmentation
Original: 4000 bytes data
Hop 1 (MTU 1500): Creates 3 fragments
├── Fragment A: Offset=0, Data=1480, MF=1
├── Fragment B: Offset=185, Data=1480, MF=1
└── Fragment C: Offset=370, Data=1040, MF=0
Hop 2 (MTU 600): Re-fragments each
Fragment A (1480 bytes) → 3 sub-fragments:
├── A1: Offset=0, Data=576, MF=1
├── A2: Offset=72, Data=576, MF=1 (72×8=576)
└── A3: Offset=144, Data=328, MF=1 (Still MF=1, more original fragments exist!)
Fragment B (1480 bytes, original offset 185) → 3 sub-fragments:
├── B1: Offset=185, Data=576, MF=1 (185×8=1480, start of Fragment B)
├── B2: Offset=257, Data=576, MF=1 (185×8 + 576 = 2056 → 257×8=2056)
└── B3: Offset=329, Data=328, MF=1
Fragment C (1040 bytes, original offset 370) → 2 sub-fragments:
├── C1: Offset=370, Data=576, MF=1
└── C2: Offset=442, Data=464, MF=0 (Last fragment, MF=0)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
def fragment_packet(data_size, original_offset, mtu, header_size=20, is_last=False): """ Fragment a packet (which may already be a fragment) at a new MTU. Offsets are always relative to the original datagram. Args: data_size: Size of data in this packet original_offset: This packet's offset in 8-byte units (0 if unfragmented) mtu: Maximum Transmission Unit at this hop header_size: IP header size is_last: Whether this is the last fragment of the original datagram Returns: List of new fragments with offsets relative to original datagram """ max_data = ((mtu - header_size) // 8) * 8 fragments = [] current_offset_bytes = original_offset * 8 # Convert to bytes remaining = data_size while remaining > 0: if remaining > max_data: frag_data = max_data # MF=1 unless this was the last fragment AND we're on its last piece mf = 1 else: frag_data = remaining # Only set MF=0 if original packet had MF=0 mf = 0 if is_last else 1 fragments.append({ 'offset_field': current_offset_bytes // 8, 'offset_bytes': current_offset_bytes, 'data_size': frag_data, 'mf': mf }) current_offset_bytes += frag_data remaining -= frag_data return fragments # Demonstrate multi-hop fragmentationprint("=" * 60)print("Hop 1: Original 4000-byte datagram, MTU=1500")print("=" * 60) hop1_fragments = fragment_packet(4000, 0, 1500, is_last=True)for i, f in enumerate(hop1_fragments): print(f"Frag {i+1}: Offset={f['offset_field']:3d}, Data={f['data_size']:4d}, MF={f['mf']}") print("\n" + "=" * 60)print("Hop 2: Re-fragment at MTU=600")print("=" * 60) for i, hop1_frag in enumerate(hop1_fragments): print(f"\nRe-fragmenting Fragment {i+1} (Offset={hop1_frag['offset_field']}, " f"Data={hop1_frag['data_size']}, MF={hop1_frag['mf']}):") is_orig_last = (hop1_frag['mf'] == 0) hop2_fragments = fragment_packet( hop1_frag['data_size'], hop1_frag['offset_field'], 600, is_last=is_orig_last ) for j, f in enumerate(hop2_fragments): suffix = " [LAST OF ORIGINAL]" if f['mf'] == 0 and is_orig_last else "" print(f" └─ {i+1}.{j+1}: Offset={f['offset_field']:3d} " f"({f['offset_bytes']:4d} bytes), Data={f['data_size']:3d}, MF={f['mf']}{suffix}")When re-fragmenting, the MF flag of sub-fragments is determined by: (1) Set MF=1 for all sub-fragments except the last sub-fragment of a packet, AND (2) Only the last sub-fragment of the ORIGINAL datagram's last fragment can have MF=0. This ensures the destination knows more data is coming.
Proper reassembly requires verifying that fragments are consistent and complete. Several checks detect malformed or malicious fragments.
Verification Checks:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
def verify_fragment_set(fragments): """ Verify a set of fragments can be correctly reassembled. Args: fragments: List of dicts with 'offset', 'data_size', 'mf' keys (offset in 8-byte units) Returns: dict with validation results """ errors = [] warnings = [] # Sort by offset sorted_frags = sorted(fragments, key=lambda f: f['offset']) # Check 1: First fragment must have offset 0 if sorted_frags[0]['offset'] != 0: errors.append(f"Missing first fragment (first offset is {sorted_frags[0]['offset']})") # Check 2: Exactly one fragment should have MF=0 last_fragments = [f for f in sorted_frags if f['mf'] == 0] if len(last_fragments) == 0: errors.append("No last fragment (all have MF=1)") elif len(last_fragments) > 1: errors.append(f"Multiple last fragments ({len(last_fragments)} have MF=0)") # Check 3: Continuity (no gaps or overlaps) for i in range(len(sorted_frags) - 1): current = sorted_frags[i] next_frag = sorted_frags[i + 1] current_end = current['offset'] * 8 + current['data_size'] next_start = next_frag['offset'] * 8 if next_start > current_end: gap = next_start - current_end errors.append(f"Gap of {gap} bytes between fragments {i+1} and {i+2}") elif next_start < current_end: overlap = current_end - next_start errors.append(f"Overlap of {overlap} bytes between fragments {i+1} and {i+2}") # Check 4: Bounds check (no fragment exceeds maximum datagram size) for i, frag in enumerate(sorted_frags): end_pos = frag['offset'] * 8 + frag['data_size'] if end_pos > 65535: errors.append(f"Fragment {i+1} exceeds max datagram size (ends at {end_pos})") # Check 5: Non-final fragments should be 8-byte aligned for i, frag in enumerate(sorted_frags): if frag['mf'] == 1 and frag['data_size'] % 8 != 0: errors.append(f"Fragment {i+1} has non-aligned size {frag['data_size']} but MF=1") # Calculate reassembled size if last_fragments and not errors: last = last_fragments[0] total_size = last['offset'] * 8 + last['data_size'] else: total_size = None return { 'valid': len(errors) == 0, 'errors': errors, 'warnings': warnings, 'total_size': total_size, 'fragment_count': len(fragments) } # Test with valid fragmentsvalid_frags = [ {'offset': 0, 'data_size': 1480, 'mf': 1}, {'offset': 185, 'data_size': 1480, 'mf': 1}, {'offset': 370, 'data_size': 560, 'mf': 0},] result = verify_fragment_set(valid_frags)print("Valid fragment set:")print(f" Valid: {result['valid']}")print(f" Total size: {result['total_size']} bytes") # Test with gapprint("\nFragment set with gap:")gap_frags = [ {'offset': 0, 'data_size': 1480, 'mf': 1}, {'offset': 200, 'data_size': 1480, 'mf': 0}, # Gap: 185*8+1480 = 2960, but 200*8 = 1600]result = verify_fragment_set(gap_frags)for error in result['errors']: print(f" Error: {error}")Understanding offset calculations is crucial for network security, as malformed offsets are vectors for attacks.
Classic Fragmentation Attacks:
| Attack | Offset Signature | Detection Method |
|---|---|---|
| Teardrop | Fragment N+1 offset < Fragment N end position | Check for overlaps during reassembly |
| Ping of Death | Last fragment offset × 8 + size > 65535 | Bounds check on all fragments |
| Tiny Fragment | First fragment size < 68 bytes (minimum TCP header) | Enforce minimum fragment size |
| Jolt/Jolt2 | Repeated identical fragments with same offset | Track fragment hash, detect duplicates |
Current best practice is to avoid fragmentation entirely using Path MTU Discovery with the DF flag set. Firewalls often drop all fragmented traffic or perform full reassembly before inspection. Understanding offset calculations helps security analysts recognize attack patterns in packet captures.
We've thoroughly covered the Fragment Offset field and its calculations. Let's consolidate the essential formulas and concepts.
12345678910111213141516171819202122232425
# Quick Reference: Offset Calculation Functions def offset_to_bytes(offset_field): """Convert Fragment Offset field value to byte position.""" return offset_field * 8 def bytes_to_offset(byte_position): """Convert byte position to Fragment Offset field value.""" assert byte_position % 8 == 0, "Position must be 8-byte aligned" return byte_position // 8 def fragment_byte_range(offset_field, data_size): """Return (first_byte, last_byte) covered by this fragment.""" first = offset_field * 8 last = first + data_size - 1 return (first, last) def next_fragment_offset(current_offset, current_data_size): """Calculate the offset field value for the next fragment.""" next_byte = current_offset * 8 + current_data_size return next_byte // 8 def original_datagram_size(last_offset, last_data_size): """Calculate original datagram DATA size from last fragment info.""" return last_offset * 8 + last_data_sizeWhat's Next:
With offset calculations mastered, the next page addresses a common practical question: How many fragments will a datagram produce? We'll develop formulas for calculating fragment counts and work through scenarios involving multiple MTU transitions.
You now understand how the Fragment Offset field precisely positions data within the original datagram, how to calculate offsets for any fragmentation scenario, and how to verify fragment set consistency. Next, we'll calculate the total number of fragments produced.