Loading content...
We've seen how RTS/CTS frames carry Duration values that 'reserve' the channel. But what actually enforces this reservation? How does a station 'know' to stay silent for a specified duration? The answer is the Network Allocation Vector (NAV)—a countdown timer maintained by every 802.11 station that implements virtual carrier sensing.
NAV is deceptively simple in concept: it's just a timer that counts down to zero. But its implementation details, update rules, and interaction with physical carrier sensing create the sophisticated channel access control that makes 802.11 function in hidden terminal environments.
The NAV is a timer maintained by each IEEE 802.11 station, indicating the predicted time until the channel becomes available. NAV is updated based on Duration/ID fields in received valid frames. When NAV > 0, the station must not attempt transmission regardless of physical carrier sense results. NAV enables 'virtual carrier sensing'—determining channel busy state from frame content rather than RF energy detection.
Why 'Virtual' Carrier Sensing?
Physical carrier sensing (also called Clear Channel Assessment or CCA) detects RF energy on the channel. If energy exceeds a threshold, the channel is 'busy.' This is a direct measurement of the wireless environment.
Virtual carrier sensing (NAV) uses information encoded in frames to determine channel state. Even if a station cannot physically detect an ongoing transmission (hidden terminal), it may have received a control frame (like CTS) that announced the reservation. NAV allows this station to honor the reservation despite not sensing the subsequent data transmission.
The key insight: Virtual carrier sensing extends the 'visibility' of channel reservations beyond the physical transmission range of individual stations.
Every 802.11 station maintains NAV as part of its MAC-layer state. Understanding the architecture helps clarify behavior.
Primary NAV Timer:
NAV Update Source Tracking:
Associated BSS Context:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
"""NAV State Machine: Complete SpecificationBased on IEEE 802.11-2020 standard behavior""" from enum import Enum, autofrom dataclasses import dataclass, fieldfrom typing import Optionalimport time class NAVUpdateSource(Enum): """Source that triggered the NAV update.""" NONE = auto() RTS = auto() CTS = auto() DATA = auto() ACK = auto() CF_END = auto() BEACON = auto() OTHER_CONTROL = auto() OTHER_DATA = auto() OTHER_MANAGEMENT = auto() @dataclassclass NAVState: """ Complete NAV state for an 802.11 station. IEEE 802.11 Spec References: - 10.3.2.4 NAV setting rules - 10.3.2.5 NAV reset rules - 10.3.2.6 NAV assertion during PCF """ # Current NAV value in microseconds nav_value_us: int = 0 # Timestamp when NAV was last updated (for countdown calculation) nav_set_time_us: int = 0 # Source of the current NAV value update_source: NAVUpdateSource = NAVUpdateSource.NONE # Address of station that triggered NAV (TA from received frame) nav_source_address: Optional[str] = None # Flag for CF (Contention Free) period NAV cf_nav_active: bool = False def get_current_tsf(self) -> int: """Get current TSF timestamp in microseconds.""" # In real implementation, this would read the hardware TSF counter return int(time.time() * 1_000_000) def get_remaining_nav(self) -> int: """Calculate remaining NAV time in microseconds.""" if self.nav_value_us == 0: return 0 elapsed = self.get_current_tsf() - self.nav_set_time_us remaining = self.nav_value_us - elapsed return max(0, remaining) def is_nav_zero(self) -> bool: """Check if NAV has expired (virtual channel idle).""" return self.get_remaining_nav() == 0 def update_nav(self, duration_us: int, source: NAVUpdateSource, source_address: Optional[str] = None) -> bool: """ Update NAV based on received Duration field. IEEE 802.11 Rule: NAV is only updated if the new value would result in a NAV expiration time later than the current value. Returns: True if NAV was updated, False if ignored """ if duration_us <= 0: return False current_remaining = self.get_remaining_nav() if duration_us > current_remaining: self.nav_value_us = duration_us self.nav_set_time_us = self.get_current_tsf() self.update_source = source self.nav_source_address = source_address return True return False def reset_nav(self, source: NAVUpdateSource = NAVUpdateSource.CF_END) -> None: """ Reset NAV to zero. Triggered by: - CF-End frame reception - NAV timeout (automatic) - Certain error recovery procedures """ self.nav_value_us = 0 self.nav_set_time_us = 0 self.update_source = source self.nav_source_address = None self.cf_nav_active = False def set_cf_nav(self, duration_us: int) -> None: """Set Contention-Free period NAV (from beacon DTIM).""" self.nav_value_us = duration_us self.nav_set_time_us = self.get_current_tsf() self.cf_nav_active = True self.update_source = NAVUpdateSource.BEACON def get_status(self) -> dict: """Get current NAV status for monitoring.""" return { 'remaining_us': self.get_remaining_nav(), 'remaining_ms': self.get_remaining_nav() / 1000, 'is_zero': self.is_nav_zero(), 'source': self.update_source.name, 'source_address': self.nav_source_address, 'cf_active': self.cf_nav_active } class ChannelAccessController: """ Combines physical and virtual carrier sensing for channel access decisions. """ def __init__(self, station_address: str): self.address = station_address self.nav = NAVState() self.phy_busy = False self.cca_threshold_dbm = -82 # Typical CCA threshold def receive_frame(self, frame_type: str, duration_us: int, ta: str, ra: str, fcs_valid: bool) -> None: """ Process a received frame for NAV update. NAV is only updated from frames with valid FCS. Special handling for frames addressed to this station. """ if not fcs_valid: # Invalid frames do not update NAV return # Determine NAV update source type source_map = { 'RTS': NAVUpdateSource.RTS, 'CTS': NAVUpdateSource.CTS, 'ACK': NAVUpdateSource.ACK, 'DATA': NAVUpdateSource.DATA, 'CF-END': NAVUpdateSource.CF_END, 'BEACON': NAVUpdateSource.BEACON, } source = source_map.get(frame_type, NAVUpdateSource.OTHER_DATA) # Special case: CF-End resets NAV if frame_type == 'CF-END': self.nav.reset_nav(NAVUpdateSource.CF_END) return # Special case: Frames addressed to us if ra == self.address: # We are the intended recipient # For CTS/ACK to us: we should not update NAV from our own exchange if frame_type in ['CTS', 'ACK']: return # Normal case: Update NAV if new duration is longer self.nav.update_nav(duration_us, source, ta) def update_phy_carrier_sense(self, energy_dbm: float) -> None: """Update physical carrier sense state based on RF energy.""" self.phy_busy = energy_dbm > self.cca_threshold_dbm def is_channel_idle(self) -> bool: """ Determine if channel is idle for transmission. Channel is idle only if BOTH: 1. Physical carrier sense indicates idle (no RF energy above threshold) 2. NAV is zero (no virtual reservation) """ phy_idle = not self.phy_busy nav_idle = self.nav.is_nav_zero() return phy_idle and nav_idle def can_start_backoff(self) -> bool: """ Determine if station can enter/continue backoff procedure. Backoff counter only decrements when channel is idle. """ return self.is_channel_idle() def get_channel_status(self) -> dict: """Get complete channel status for monitoring.""" return { 'phy_busy': self.phy_busy, 'nav_status': self.nav.get_status(), 'channel_idle': self.is_channel_idle(), 'can_transmit': self.is_channel_idle() } # Demonstrationdef demonstrate_nav_behavior(): """Show NAV behavior through a complete RTS/CTS/Data/ACK exchange.""" print("=== NAV Behavior Demonstration ===") print() # Create three stations controller_sender = ChannelAccessController("AA:AA:AA:AA:AA:AA") controller_hidden = ChannelAccessController("BB:BB:BB:BB:BB:BB") controller_visible = ChannelAccessController("CC:CC:CC:CC:CC:CC") # Initial state print("--- Initial State ---") print(f"Hidden station NAV: {controller_hidden.nav.get_remaining_nav()} μs") print(f"Visible station NAV: {controller_visible.nav.get_remaining_nav()} μs") print() # Simulate RTS reception at visible station (hidden doesn't hear it) print("--- RTS Transmitted (only visible station hears) ---") controller_visible.receive_frame( frame_type='RTS', duration_us=2000, # Duration until end of ACK ta="AA:AA:AA:AA:AA:AA", ra="DD:DD:DD:DD:DD:DD", # AP fcs_valid=True ) print(f"Hidden station NAV: {controller_hidden.nav.get_remaining_nav()} μs (did not hear RTS)") print(f"Visible station NAV: {controller_visible.nav.get_remaining_nav()} μs") print() # Simulate CTS reception (both stations hear from AP) print("--- CTS from AP (both stations hear) ---") cts_duration = 2000 - 44 - 16 # RTS duration minus CTS time minus SIFS controller_hidden.receive_frame( frame_type='CTS', duration_us=cts_duration, ta="DD:DD:DD:DD:DD:DD", # AP ra="AA:AA:AA:AA:AA:AA", # Original sender fcs_valid=True ) controller_visible.receive_frame( frame_type='CTS', duration_us=cts_duration, ta="DD:DD:DD:DD:DD:DD", ra="AA:AA:AA:AA:AA:AA", fcs_valid=True ) print(f"Hidden station NAV: {controller_hidden.nav.get_remaining_nav()} μs (updated from CTS)") print(f"Visible station NAV: {controller_visible.nav.get_remaining_nav()} μs (kept from RTS, higher)") print() print("--- Can Stations Transmit? ---") print(f"Hidden station can transmit: {controller_hidden.is_channel_idle()}") print(f"Visible station can transmit: {controller_visible.is_channel_idle()}") demonstrate_nav_behavior()Unlike physical carrier sensing (which reflects instantaneous RF conditions), NAV persists based on time. A station may update NAV from a received frame, then move to a location where it can no longer receive any related frames—yet NAV continues to count down and enforce deferral. This 'memory' is what makes virtual carrier sensing effective for hidden terminals.
Every 802.11 frame's MAC header contains a 16-bit Duration/ID field. This field's interpretation depends on the frame type and its bit patterns.
Bit 15:
When Bit 15 = 0 (Duration):
When Bit 15 = 1:
| Bit 15 | Bit 14 | Bits 13-0 | Interpretation |
|---|---|---|---|
| 0 | 0 | Duration value | NAV update (0-16383 μs) |
| 0 | 1 | Duration value | NAV update (16384-32767 μs) |
| 1 | 0 | X | Fixed duration / Reserved |
| 1 | 1 | AID value | Association ID (PS-Poll) |
| 1 | 1 | All 1s | Null (Reserved) |
RTS (Request-to-Send):
Duration = 3×SIFS + CTS_time + Data_time + ACK_time
This encompasses the entire remaining exchange after RTS.
CTS (Clear-to-Send):
Duration = RTS_Duration - SIFS - CTS_time
Decremented to reflect time consumed by CTS transmission.
Data Frames:
Duration = SIFS + ACK_time
Protects only the ACK response.
ACK Frames:
Duration = 0
No further protection needed; exchange complete.
Special Cases:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
"""Duration Field Calculation for Different Frame Types802.11 MAC layer timing analysis""" # Timing constants (802.11g/n with OFDM)SIFS_US = 16 # Short Interframe SpaceSLOT_TIME_US = 9 # Slot timeCTS_TIME_US = 44 # CTS transmission time at 6 MbpsACK_TIME_US = 44 # ACK transmission time at 6 MbpsRTS_TIME_US = 52 # RTS transmission time at 6 Mbps def calculate_ofdm_txtime(bytes: int, rate_mbps: float) -> float: """Calculate OFDM transmission time in microseconds.""" import math # PLCP preamble and header: 20 μs # Symbol time: 4 μs # Bits per symbol = rate_mbps × 4 preamble = 20 bits = 16 + (bytes * 8) + 6 # Service + MPDU + Tail bits_per_symbol = rate_mbps * 4 n_symbols = math.ceil(bits / bits_per_symbol) return preamble + (n_symbols * 4) def calculate_rts_duration(data_bytes: int, data_rate_mbps: float) -> int: """ Calculate Duration field value for RTS frame. Duration = SIFS + CTS + SIFS + DATA + SIFS + ACK """ data_time = calculate_ofdm_txtime(data_bytes + 38, data_rate_mbps) # +38 for MAC header/FCS duration = (SIFS_US + CTS_TIME_US + SIFS_US + data_time + SIFS_US + ACK_TIME_US) return int(duration) def calculate_cts_duration(rts_duration: int) -> int: """ Calculate Duration field value for CTS frame. Duration = RTS_Duration - SIFS - CTS_time """ return max(0, rts_duration - SIFS_US - CTS_TIME_US) def calculate_data_duration() -> int: """ Calculate Duration field value for unicast Data frame. Duration = SIFS + ACK """ return SIFS_US + ACK_TIME_US def display_duration_chain(data_bytes: int, data_rate_mbps: float): """Display the Duration field values across an RTS/CTS/Data/ACK exchange.""" rts_duration = calculate_rts_duration(data_bytes, data_rate_mbps) cts_duration = calculate_cts_duration(rts_duration) data_duration = calculate_data_duration() ack_duration = 0 # ACK has Duration = 0 print(f"=== Duration Field Chain ({data_bytes} bytes @ {data_rate_mbps} Mbps) ===") print() print("Frame | Duration (μs) | Protects") print("-" * 50) print(f"RTS | {rts_duration:>13} | CTS + Data + ACK") print(f"CTS | {cts_duration:>13} | Data + ACK") print(f"Data | {data_duration:>13} | ACK only") print(f"ACK | {ack_duration:>13} | Nothing (end)") print() print("NAV decreases through the exchange:") print(f" After RTS heard: NAV = {rts_duration} μs") print(f" After CTS heard: NAV = max(current, {cts_duration}) μs") print(f" After Data heard: NAV = max(current, {data_duration}) μs") print(f" After ACK heard: NAV → 0 (expired or stays current)") # Examplesprint("Example 1: Small Frame (256 bytes @ 54 Mbps)")display_duration_chain(256, 54)print() print("Example 2: Large Frame (1500 bytes @ 54 Mbps)")display_duration_chain(1500, 54)print() print("Example 3: Maximum Frame (2304 bytes @ 6 Mbps)")display_duration_chain(2304, 6)Notice how Duration decreases through the frame exchange. This 'decrementing chain' ensures that stations receiving only later frames in the sequence still set appropriate NAV. If a hidden terminal misses RTS but receives CTS, it gets the correct remaining reservation time.
The IEEE 802.11 standard defines precise rules for when NAV is updated. Understanding these rules prevents subtle bugs in implementation and clarifies behavior analysis.
Rule 1: NAV is only updated if the new expiration time would be later than current.
New_NAV_Expiration = Current_Time + Received_Duration Current_NAV_Expiration = NAV_Set_Time + Current_NAV_Value
Update NAV iff: New_NAV_Expiration > Current_NAV_Expiration
Why this rule?
Multiple overlapping exchanges may occur. A station might receive:
Without Rule 1, the CTS would reduce NAV from 2000 to 1900 μs. With Rule 1, the station keeps the longer reservation from RTS. This is correct because both values protect the same exchange—the CTS Duration is already accounted for in the RTS Duration.
RTS Frames:
CTS Frames:
Data Frames:
ACK Frames:
Beacon Frames:
| Frame Type | Intended Receiver Updates NAV? | Other Stations Update NAV? | Special Notes |
|---|---|---|---|
| RTS | No (will respond) | Yes | Duration covers full exchange |
| CTS | No (original sender) | Yes | Duration = RTS.Duration - CTS - SIFS |
| Unicast Data | No | Yes | Duration = SIFS + ACK |
| Broadcast Data | N/A | No (Duration=0) | No ACK expected |
| ACK | N/A | No (Duration=0) | Exchange complete |
| CF-End | N/A | Reset to 0 | Explicit NAV reset |
| Beacon | N/A | Maybe (CF params) | Sets NAV for PCF if CF Param present |
In rare cases, NAV updates from different sources can interact unexpectedly. Example: Station receives RTS (NAV=2000), then before CTS arrives, receives a long management frame from another exchange (NAV=5000). The second frame's larger Duration prevails, which is correct—both channels need protection.
NAV decrements continuously and eventually reaches zero. However, explicit reset mechanisms exist for specific situations.
The only frame that explicitly resets NAV to zero is the CF-End (Contention-Free End) frame:
Purpose:
Usage:
Why not use Duration = 0 in other frames?
NAV Countdown:
Timeout Protection Against Stale NAV:
802.11 includes a safeguard against indefinitely stale NAV values:
If NAV was set from an RTS that didn't result in expected CTS:
Potential Issue: Extremely long Duration values
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
"""NAV Reset Scenarios and Edge Cases""" from enum import Enum, autofrom dataclasses import dataclassfrom typing import Optional class NAVResetScenario(Enum): """Different scenarios that affect NAV state.""" NORMAL_EXPIRATION = auto() # NAV counts down to zero CF_END_RECEIVED = auto() # Explicit reset from CF-End EXCHANGE_FAILURE = auto() # RTS sent, no CTS received MANAGEMENT_RESET = auto() # Disassociation/deauth CHANNEL_SWITCH = auto() # Moving to different channel @dataclassclass NAVEvent: """Represents a NAV-related event.""" timestamp_us: int event_type: str nav_before: int nav_after: int reason: str class NAVResilienceAnalyzer: """Analyze NAV behavior in various scenarios.""" def __init__(self): self.events: list[NAVEvent] = [] self.current_nav = 0 self.nav_set_time = 0 def simulate_normal_exchange(self, base_time: int): """Simulate successful RTS/CTS/Data/ACK exchange.""" # RTS sets NAV to 2000 μs self.events.append(NAVEvent( timestamp_us=base_time, event_type="RTS_RECEIVED", nav_before=self.current_nav, nav_after=2000, reason="Duration field = 2000" )) self.current_nav = 2000 self.nav_set_time = base_time # CTS at +60 μs (would set NAV = 1900, but current is higher) at_cts = base_time + 60 remaining = self.current_nav - 60 # 1940 cts_duration = 1900 # From CTS self.events.append(NAVEvent( timestamp_us=at_cts, event_type="CTS_RECEIVED", nav_before=remaining, nav_after=remaining, # Not updated (1940 > 1900) reason=f"CTS Duration {cts_duration} < remaining {remaining}; no update" )) # NAV expires naturally expiration_time = base_time + 2000 self.events.append(NAVEvent( timestamp_us=expiration_time, event_type="NAV_EXPIRED", nav_before=0, nav_after=0, reason="Natural countdown to zero" )) return self.events def simulate_rts_failure(self, base_time: int): """Simulate RTS without CTS response (failed exchange).""" # RTS sets NAV to 2000 μs self.events.append(NAVEvent( timestamp_us=base_time, event_type="RTS_RECEIVED", nav_before=self.current_nav, nav_after=2000, reason="Duration field = 2000" )) self.current_nav = 2000 self.nav_set_time = base_time # CTS never arrives self.events.append(NAVEvent( timestamp_us=base_time + 200, event_type="CTS_TIMEOUT", nav_before=1800, # 2000 - 200 nav_after=1800, # No change from timeout reason="Sender notices timeout; NAV in other stations unchanged" )) # NAV still expires at original time # This is 'wasted' but ensures protection even for partial exchanges expiration_time = base_time + 2000 self.events.append(NAVEvent( timestamp_us=expiration_time, event_type="NAV_EXPIRED", nav_before=0, nav_after=0, reason="Natural expiration despite no data transfer" )) return self.events def simulate_cf_end_reset(self, base_time: int): """Simulate CF period with CF-End reset.""" # Beacon sets NAV for CFP (5000 μs) self.events.append(NAVEvent( timestamp_us=base_time, event_type="BEACON_CF_PARAMS", nav_before=self.current_nav, nav_after=5000, reason="CFP Duration from beacon = 5000" )) self.current_nav = 5000 # CFP ends early, CF-End sent at +3000 μs cf_end_time = base_time + 3000 self.events.append(NAVEvent( timestamp_us=cf_end_time, event_type="CF_END_RECEIVED", nav_before=2000, # Would be 5000 - 3000 nav_after=0, # Explicit reset! reason="CF-End explicitly resets NAV to 0" )) self.current_nav = 0 return self.events def display_events(self): """Display all recorded events.""" print("=== NAV Event Log ===") print() for event in self.events: print(f"T={event.timestamp_us:>8} μs | {event.event_type:20} | " f"NAV: {event.nav_before:>5} → {event.nav_after:>5} | {event.reason}") # Demonstrate scenariosanalyzer1 = NAVResilienceAnalyzer()analyzer1.simulate_normal_exchange(0)analyzer1.display_events() print() analyzer2 = NAVResilienceAnalyzer()analyzer2.simulate_cf_end_reset(10000)analyzer2.display_events()In 802.11e/WMM (QoS), TXOP (Transmission Opportunity) allows multiple frame bursts. The first frame's Duration covers the entire TXOP. If exchange ends early, the TXOP holder may send CF-End or 'EIFS' to signal early release. This is an advanced NAV reset mechanism.
Channel access in 802.11 requires coordination between physical (CCA) and virtual (NAV) carrier sensing. Understanding their interaction is essential for complete protocol comprehension.
Method 1: Energy Detection (ED)
Method 2: Carrier Sense (CS)
CCA Result = Busy if (ED busy OR CS detected))
A station determines channel state by combining both mechanisms:
Channel Busy = (CCA indicates busy) OR (NAV > 0)
Channel Idle = (CCA indicates idle) AND (NAV = 0)
Implications:
Physical busy, NAV idle: Someone nearby is transmitting (but not protected via virtual sensing). Station defers.
Physical idle, NAV busy: Channel seems clear physically, but a reservation exists. Station defers.
Both busy: Normal case during protected exchange. Clear deferral requirement.
Both idle: Channel is available. Station may begin backoff or transmit.
Case 1: NAV set, then physical carries clear
Scenario: Station received CTS (sets NAV). CTS sender finishes and data exchange continues at hidden transmitter. Station's physical sensing shows idle.
Behavior: Station defers based on NAV. This is correct—data is in flight at hidden location.
Case 2: Physical busy without NAV
Scenario: A nearby non-802.11 device (microwave, Bluetooth) creates RF energy. No valid 802.11 frames received.
Behavior: Station defers based on ED (energy detection). NAV remains 0. When interference stops, immediate access opportunity.
Case 3: Spoofed high Duration value
Scenario: Malicious/buggy device sends Valid frame with absurdly high Duration (32,767 μs).
Behavior: All hearing stations set NAV = 32,767 μs (~33 ms denial of service per frame). This is a known vulnerability.
NAV-based DoS attacks exploit the trust model of Duration fields. By broadcasting frames with maximum Duration values, an attacker can force nearby stations to defer for 33ms per frame. Continuous transmission at low duty cycle can severely degrade network throughput. Defenses include NAV maximum enforcement and anomaly detection.
NAV behavior varies slightly across different 802.11 operating modes and standards. Understanding these variations is important for comprehensive knowledge.
Standard mode where NAV operates as described:
CF Parameter Set in Beacon:
CF-End Behavior:
CF-Ack:
EDCA (Enhanced Distributed Channel Access):
HCCA (Controlled Channel Access):
Intra-BSS NAV vs Inter-BSS NAV:
802.11ax introduces the concept of BSS Coloring and potentially separate NAV handling:
Note: This is an active area of implementation variation.
Trigger-Based NAV:
| Standard/Mode | Primary NAV Sources | Special Behavior |
|---|---|---|
| 802.11 DCF | RTS, CTS, Data | Standard virtual carrier sensing |
| 802.11 PCF | Beacon CFP, CF-End | NAV protects CFP; CF-End resets |
| 802.11e EDCA | RTS, CTS, TXOP | TXOP burst protection via NAV |
| 802.11n/ac | Same as EDCA + block ACK | Extended NAV for aggregation |
| 802.11ax | Same + Trigger, MU-RTS | BSS Color influences NAV interpretation |
| 802.11ax SR | Potentially ignored for OBSS | Spatial Reuse may override NAV |
In mesh networks (802.11s) and IBSS (ad-hoc), NAV operates identically but has increased importance due to multi-hop hidden terminals. Mesh peer link management may use NAV for protection during handshake exchanges.
For network engineers and researchers, monitoring NAV behavior provides insights into channel utilization and potential issues.
1. Protocol Analyzer (Wireshark with Monitor Mode)
2. Vendor-Specific Diagnostics
3. Custom Monitor Mode Station
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
"""NAV Analysis Script for Wireshark/tshark CapturesAnalyzes Duration fields and NAV behavior""" import subprocessimport jsonfrom collections import defaultdictfrom datetime import datetime def analyze_nav_from_capture(pcap_file: str) -> dict: """ Analyze NAV-related fields from a packet capture. Requires tshark installed and capture taken in monitor mode. """ # tshark command to extract Duration and frame info cmd = [ 'tshark', '-r', pcap_file, '-T', 'json', '-e', 'frame.time_relative', '-e', 'wlan.fc.type', '-e', 'wlan.fc.subtype', '-e', 'wlan.duration', '-e', 'wlan.ta', '-e', 'wlan.ra', '-Y', 'wlan' # Filter to WLAN frames only ] # This would run tshark - for demonstration, we'll simulate data # result = subprocess.run(cmd, capture_output=True, text=True) # frames = json.loads(result.stdout) # Simulated frame data for demonstration frames = [ {'time': 0.0, 'type': 1, 'subtype': 11, 'duration': 500, 'ta': 'AA:AA:AA:AA:AA:AA'}, {'time': 0.00005, 'type': 1, 'subtype': 12, 'duration': 440, 'ta': 'BB:BB:BB:BB:BB:BB'}, {'time': 0.0001, 'type': 2, 'subtype': 0, 'duration': 60, 'ta': 'AA:AA:AA:AA:AA:AA'}, {'time': 0.0005, 'type': 1, 'subtype': 13, 'duration': 0, 'ta': 'BB:BB:BB:BB:BB:BB'}, {'time': 0.001, 'type': 1, 'subtype': 11, 'duration': 1200, 'ta': 'CC:CC:CC:CC:CC:CC'}, {'time': 0.0015, 'type': 1, 'subtype': 11, 'duration': 32767, 'ta': 'DD:DD:DD:DD:DD:DD'}, # Suspicious! ] analysis = { 'total_frames': len(frames), 'nav_updates': 0, 'max_duration_seen': 0, 'duration_histogram': defaultdict(int), 'suspicious_frames': [], 'frame_type_duration': defaultdict(list) } frame_type_names = { (1, 11): 'RTS', (1, 12): 'CTS', (1, 13): 'ACK', (2, 0): 'Data', (0, 8): 'Beacon' } for frame in frames: duration = frame.get('duration', 0) ftype = frame.get('type', 0) subtype = frame.get('subtype', 0) frame_name = frame_type_names.get((ftype, subtype), f'Type{ftype}/Sub{subtype}') if duration > 0: analysis['nav_updates'] += 1 analysis['max_duration_seen'] = max(analysis['max_duration_seen'], duration) # Histogram in 100μs buckets bucket = (duration // 100) * 100 analysis['duration_histogram'][bucket] += 1 analysis['frame_type_duration'][frame_name].append(duration) # Flag suspicious Duration values (> 10ms unusual for normal operation) if duration > 10000: analysis['suspicious_frames'].append({ 'time': frame.get('time'), 'type': frame_name, 'duration': duration, 'ta': frame.get('ta'), 'warning': 'Unusually high Duration value' }) return analysis def display_nav_analysis(analysis: dict): """Display NAV analysis results.""" print("=== NAV Analysis Report ===") print() print(f"Total frames analyzed: {analysis['total_frames']}") print(f"Frames with NAV updates: {analysis['nav_updates']}") print(f"Maximum Duration seen: {analysis['max_duration_seen']} μs") print() print("--- Duration by Frame Type ---") for frame_type, durations in analysis['frame_type_duration'].items(): if durations: avg = sum(durations) / len(durations) print(f" {frame_type:10}: count={len(durations):4}, " f"avg={avg:7.1f} μs, max={max(durations):6} μs") print() if analysis['suspicious_frames']: print("!!! SUSPICIOUS FRAMES DETECTED !!!") for sf in analysis['suspicious_frames']: print(f" T={sf['time']:.6f}s: {sf['type']} from {sf['ta']}, " f"Duration={sf['duration']} μs") print(f" Warning: {sf['warning']}") print() else: print("No suspicious frames detected.") print("--- Duration Histogram (100μs buckets) ---") for bucket in sorted(analysis['duration_histogram'].keys()): count = analysis['duration_histogram'][bucket] bar = '█' * min(count, 50) print(f" {bucket:>6}-{bucket+99:>5} μs: {count:>4} {bar}") # Run analysisanalysis = analyze_nav_from_capture("sample_capture.pcap")display_nav_analysis(analysis)Common NAV-related issues: (1) High Duration values from misconfigured devices → check for firmware updates, (2) NAV not being set → clients not receiving RTS/CTS, check coverage, (3) Excessive NAV-based deferral from neighboring BSS → implement channel planning or enable BSS Coloring (802.11ax).
We've exhaustively examined the NAV mechanism—the core of virtual carrier sensing that enables hidden terminal protection. Let's consolidate the essential knowledge:
The final page of this module explores Virtual Sensing as a complete mechanism—integrating NAV with broader protocol behavior, examining EIFS, and analyzing how virtual and physical carrier sensing work together for comprehensive channel protection.
You now possess deep knowledge of the NAV mechanism: its architecture, update rules, reset conditions, Duration field calculations, and interaction with physical carrier sensing. This understanding is essential for both 802.11 protocol mastery and GATE/competitive exam preparation.