Skip to content

Commit 9b4cc3f

Browse files
committed
channel: add synchronization
1 parent 4269ca4 commit 9b4cc3f

2 files changed

Lines changed: 371 additions & 0 deletions

File tree

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
Unit tests for channel synchronization in dual interface WPA attacks.
6+
7+
Tests the channel synchronization functionality including:
8+
- Channel verification on both interfaces
9+
- Handling of channel mismatch
10+
- Recovery from channel setting failures
11+
"""
12+
13+
import unittest
14+
import sys
15+
from unittest.mock import Mock, patch, MagicMock, call
16+
17+
# Mock sys.argv to prevent argparse from reading test arguments
18+
original_argv = sys.argv
19+
sys.argv = ['wifite']
20+
21+
from wifite.config import Configuration
22+
from wifite.model.target import Target
23+
24+
# Set required Configuration attributes
25+
Configuration.interface = 'wlan0'
26+
Configuration.wpa_attack_timeout = 600
27+
Configuration.wpa_deauth_timeout = 10
28+
Configuration.interface_primary = None
29+
Configuration.interface_secondary = None
30+
Configuration.dual_interface_enabled = False
31+
32+
from wifite.attack.wpa import AttackWPA
33+
34+
# Restore original argv
35+
sys.argv = original_argv
36+
37+
38+
class TestChannelSynchronization(unittest.TestCase):
39+
"""Test channel synchronization for dual interface WPA attacks."""
40+
41+
def setUp(self):
42+
"""Set up test fixtures."""
43+
self.mock_target = Mock(spec=Target)
44+
self.mock_target.bssid = 'AA:BB:CC:DD:EE:FF'
45+
self.mock_target.essid = 'TestNetwork'
46+
self.mock_target.channel = 6
47+
self.mock_target.encryption = 'WPA2'
48+
self.mock_target.power = -50
49+
self.mock_target.wps = False
50+
51+
Configuration.interface = 'wlan0'
52+
Configuration.interface_primary = None
53+
Configuration.interface_secondary = None
54+
55+
@patch('wifite.util.interface_manager.InterfaceManager')
56+
@patch('wifite.attack.wpa.Color')
57+
def test_verify_channel_sync_both_correct(self, mock_color, mock_interface_manager):
58+
"""Test channel verification when both interfaces are on correct channel."""
59+
# Setup
60+
attack = AttackWPA(self.mock_target)
61+
attack.capture_interface = 'wlan0mon'
62+
attack.deauth_interface = 'wlan1mon'
63+
attack.view = None
64+
65+
# Mock both interfaces on correct channel
66+
mock_interface_manager._get_interface_channel.side_effect = [6, 6]
67+
68+
# Execute
69+
attack._verify_channel_sync()
70+
71+
# Verify
72+
self.assertEqual(mock_interface_manager._get_interface_channel.call_count, 2)
73+
mock_interface_manager._get_interface_channel.assert_any_call('wlan0mon')
74+
mock_interface_manager._get_interface_channel.assert_any_call('wlan1mon')
75+
76+
# Should show success message
77+
success_calls = [c for c in mock_color.pl.call_args_list
78+
if 'Both interfaces verified' in str(c)]
79+
self.assertGreater(len(success_calls), 0)
80+
81+
@patch('wifite.util.interface_manager.InterfaceManager')
82+
@patch('wifite.attack.wpa.Color')
83+
def test_verify_channel_sync_capture_mismatch(self, mock_color, mock_interface_manager):
84+
"""Test channel verification when capture interface is on wrong channel."""
85+
# Setup
86+
attack = AttackWPA(self.mock_target)
87+
attack.capture_interface = 'wlan0mon'
88+
attack.deauth_interface = 'wlan1mon'
89+
attack.view = None
90+
91+
# Mock capture interface on wrong channel (11 instead of 6)
92+
mock_interface_manager._get_interface_channel.side_effect = [11, 6]
93+
94+
# Execute
95+
attack._verify_channel_sync()
96+
97+
# Verify warning was shown
98+
warning_calls = [c for c in mock_color.pl.call_args_list
99+
if 'Warning' in str(c) and 'wlan0mon' in str(c)]
100+
self.assertGreater(len(warning_calls), 0)
101+
102+
@patch('wifite.util.interface_manager.InterfaceManager')
103+
@patch('wifite.attack.wpa.Color')
104+
def test_verify_channel_sync_deauth_mismatch(self, mock_color, mock_interface_manager):
105+
"""Test channel verification when deauth interface is on wrong channel."""
106+
# Setup
107+
attack = AttackWPA(self.mock_target)
108+
attack.capture_interface = 'wlan0mon'
109+
attack.deauth_interface = 'wlan1mon'
110+
attack.view = None
111+
112+
# Mock deauth interface on wrong channel (1 instead of 6)
113+
mock_interface_manager._get_interface_channel.side_effect = [6, 1]
114+
115+
# Execute
116+
attack._verify_channel_sync()
117+
118+
# Verify warning was shown
119+
warning_calls = [c for c in mock_color.pl.call_args_list
120+
if 'Warning' in str(c) and 'wlan1mon' in str(c)]
121+
self.assertGreater(len(warning_calls), 0)
122+
123+
@patch('wifite.util.interface_manager.InterfaceManager')
124+
@patch('wifite.attack.wpa.Color')
125+
def test_verify_channel_sync_both_mismatch(self, mock_color, mock_interface_manager):
126+
"""Test channel verification when both interfaces are on wrong channels."""
127+
# Setup
128+
attack = AttackWPA(self.mock_target)
129+
attack.capture_interface = 'wlan0mon'
130+
attack.deauth_interface = 'wlan1mon'
131+
attack.view = None
132+
133+
# Mock both interfaces on wrong channels
134+
mock_interface_manager._get_interface_channel.side_effect = [11, 1]
135+
136+
# Execute
137+
attack._verify_channel_sync()
138+
139+
# Verify warnings for both interfaces
140+
warning_calls = [c for c in mock_color.pl.call_args_list
141+
if 'Warning' in str(c)]
142+
self.assertGreaterEqual(len(warning_calls), 2)
143+
144+
@patch('wifite.util.process.Process')
145+
@patch('wifite.attack.wpa.Color')
146+
def test_set_interface_channels_both_success(self, mock_color, mock_process):
147+
"""Test setting channels when both interfaces succeed."""
148+
# Setup
149+
attack = AttackWPA(self.mock_target)
150+
attack.capture_interface = 'wlan0mon'
151+
attack.deauth_interface = 'wlan1mon'
152+
attack.view = None
153+
154+
# Mock successful process execution
155+
mock_proc = MagicMock()
156+
mock_process.return_value = mock_proc
157+
158+
# Execute
159+
attack._set_interface_channels()
160+
161+
# Verify both interfaces were set
162+
self.assertEqual(mock_process.call_count, 2)
163+
mock_process.assert_any_call(['iw', 'wlan0mon', 'set', 'channel', '6'])
164+
mock_process.assert_any_call(['iw', 'wlan1mon', 'set', 'channel', '6'])
165+
166+
# Should show success message
167+
success_calls = [c for c in mock_color.pl.call_args_list
168+
if 'Both interfaces set' in str(c)]
169+
self.assertGreater(len(success_calls), 0)
170+
171+
@patch('wifite.util.process.Process')
172+
@patch('wifite.attack.wpa.Color')
173+
def test_set_interface_channels_capture_fails(self, mock_color, mock_process):
174+
"""Test setting channels when capture interface fails."""
175+
# Setup
176+
attack = AttackWPA(self.mock_target)
177+
attack.capture_interface = 'wlan0mon'
178+
attack.deauth_interface = 'wlan1mon'
179+
attack.view = None
180+
181+
# Mock first call (capture) fails, second (deauth) succeeds
182+
def process_side_effect(cmd):
183+
mock_proc = MagicMock()
184+
if 'wlan0mon' in cmd:
185+
mock_proc.wait.side_effect = Exception('Channel set failed')
186+
return mock_proc
187+
188+
mock_process.side_effect = process_side_effect
189+
190+
# Execute
191+
attack._set_interface_channels()
192+
193+
# Verify error was logged
194+
error_calls = [c for c in mock_color.pl.call_args_list
195+
if 'Error' in str(c) and 'wlan0mon' in str(c)]
196+
self.assertGreater(len(error_calls), 0)
197+
198+
# Should show warning about only one interface working
199+
warning_calls = [c for c in mock_color.pl.call_args_list
200+
if 'Only' in str(c) and 'wlan1mon' in str(c)]
201+
self.assertGreater(len(warning_calls), 0)
202+
203+
@patch('wifite.util.process.Process')
204+
@patch('wifite.attack.wpa.Color')
205+
def test_set_interface_channels_deauth_fails(self, mock_color, mock_process):
206+
"""Test setting channels when deauth interface fails."""
207+
# Setup
208+
attack = AttackWPA(self.mock_target)
209+
attack.capture_interface = 'wlan0mon'
210+
attack.deauth_interface = 'wlan1mon'
211+
attack.view = None
212+
213+
# Mock first call (capture) succeeds, second (deauth) fails
214+
def process_side_effect(cmd):
215+
mock_proc = MagicMock()
216+
if 'wlan1mon' in cmd:
217+
mock_proc.wait.side_effect = Exception('Channel set failed')
218+
return mock_proc
219+
220+
mock_process.side_effect = process_side_effect
221+
222+
# Execute
223+
attack._set_interface_channels()
224+
225+
# Verify error was logged
226+
error_calls = [c for c in mock_color.pl.call_args_list
227+
if 'Error' in str(c) and 'wlan1mon' in str(c)]
228+
self.assertGreater(len(error_calls), 0)
229+
230+
# Should show warning about only one interface working
231+
warning_calls = [c for c in mock_color.pl.call_args_list
232+
if 'Only' in str(c) and 'wlan0mon' in str(c)]
233+
self.assertGreater(len(warning_calls), 0)
234+
235+
@patch('wifite.util.process.Process')
236+
@patch('wifite.attack.wpa.Color')
237+
def test_set_interface_channels_both_fail(self, mock_color, mock_process):
238+
"""Test setting channels when both interfaces fail."""
239+
# Setup
240+
attack = AttackWPA(self.mock_target)
241+
attack.capture_interface = 'wlan0mon'
242+
attack.deauth_interface = 'wlan1mon'
243+
attack.view = None
244+
245+
# Mock both calls fail
246+
mock_proc = MagicMock()
247+
mock_proc.wait.side_effect = Exception('Channel set failed')
248+
mock_process.return_value = mock_proc
249+
250+
# Execute
251+
attack._set_interface_channels()
252+
253+
# Verify error messages for both interfaces
254+
error_calls = [c for c in mock_color.pl.call_args_list
255+
if 'Error' in str(c)]
256+
self.assertGreaterEqual(len(error_calls), 2)
257+
258+
# Should show error about both interfaces failing
259+
both_fail_calls = [c for c in mock_color.pl.call_args_list
260+
if 'Failed to set channel on both' in str(c)]
261+
self.assertGreater(len(both_fail_calls), 0)
262+
263+
264+
if __name__ == '__main__':
265+
unittest.main()

wifite/attack/wpa.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,12 @@ def _run_dual_interface(self):
327327
if self.view:
328328
self.view.add_log(f"Monitor mode enabled: {self.capture_interface}, {self.deauth_interface}")
329329

330+
# Set both interfaces to target channel with error handling
331+
self._set_interface_channels()
332+
333+
# Verify both interfaces are on the target channel
334+
self._verify_channel_sync()
335+
330336
# Route to appropriate capture method based on configuration
331337
if use_hcxdump_mode:
332338
Color.pl('{+} {C}Using hcxdumptool capture method{W}')
@@ -520,6 +526,106 @@ def _capture_handshake_dual_airodump(self):
520526

521527
return handshake
522528

529+
def _set_interface_channels(self):
530+
"""
531+
Set both interfaces to the target channel with error handling.
532+
533+
Attempts to set both capture and deauth interfaces to the target channel.
534+
If one interface fails, logs an error but continues with the working interface.
535+
"""
536+
from ..util.process import Process
537+
from ..util.logger import log_error, log_debug, log_info
538+
539+
target_channel = self.target.channel
540+
capture_success = False
541+
deauth_success = False
542+
543+
# Set capture interface channel
544+
try:
545+
log_debug('AttackWPA', f'Setting {self.capture_interface} to channel {target_channel}')
546+
Process(['iw', self.capture_interface, 'set', 'channel', str(target_channel)]).wait()
547+
capture_success = True
548+
log_info('AttackWPA', f'Successfully set {self.capture_interface} to channel {target_channel}')
549+
except Exception as e:
550+
error_msg = f'Failed to set channel on {self.capture_interface}: {e}'
551+
log_error('AttackWPA', error_msg, e)
552+
Color.pl('{!} {R}Error: %s{W}' % error_msg)
553+
if self.view:
554+
self.view.add_log(f'Error: {error_msg}')
555+
556+
# Set deauth interface channel
557+
try:
558+
log_debug('AttackWPA', f'Setting {self.deauth_interface} to channel {target_channel}')
559+
Process(['iw', self.deauth_interface, 'set', 'channel', str(target_channel)]).wait()
560+
deauth_success = True
561+
log_info('AttackWPA', f'Successfully set {self.deauth_interface} to channel {target_channel}')
562+
except Exception as e:
563+
error_msg = f'Failed to set channel on {self.deauth_interface}: {e}'
564+
log_error('AttackWPA', error_msg, e)
565+
Color.pl('{!} {R}Error: %s{W}' % error_msg)
566+
if self.view:
567+
self.view.add_log(f'Error: {error_msg}')
568+
569+
# Report overall status
570+
if capture_success and deauth_success:
571+
Color.pl('{+} {G}Both interfaces set to channel %d{W}' % target_channel)
572+
if self.view:
573+
self.view.add_log(f'Both interfaces set to channel {target_channel}')
574+
elif capture_success or deauth_success:
575+
working_iface = self.capture_interface if capture_success else self.deauth_interface
576+
Color.pl('{!} {O}Warning: Only %s successfully set to channel %d{W}' % (working_iface, target_channel))
577+
Color.pl('{!} {O}Continuing with working interface...{W}')
578+
if self.view:
579+
self.view.add_log(f'Warning: Only {working_iface} on channel {target_channel}')
580+
else:
581+
Color.pl('{!} {R}Error: Failed to set channel on both interfaces{W}')
582+
if self.view:
583+
self.view.add_log('Error: Failed to set channel on both interfaces')
584+
585+
def _verify_channel_sync(self):
586+
"""
587+
Verify both interfaces are on the target channel.
588+
589+
Checks the current channel of both capture and deauth interfaces
590+
and logs a warning if they don't match the target channel.
591+
"""
592+
from ..util.interface_manager import InterfaceManager
593+
from ..util.logger import log_warning, log_debug
594+
595+
try:
596+
# Get current channel of both interfaces
597+
capture_channel = InterfaceManager._get_interface_channel(self.capture_interface)
598+
deauth_channel = InterfaceManager._get_interface_channel(self.deauth_interface)
599+
target_channel = self.target.channel
600+
601+
log_debug('AttackWPA', f'Channel verification: target={target_channel}, capture={capture_channel}, deauth={deauth_channel}')
602+
603+
# Check if capture interface is on target channel
604+
if capture_channel != target_channel:
605+
warning_msg = f'Capture interface {self.capture_interface} is on channel {capture_channel}, expected {target_channel}'
606+
log_warning('AttackWPA', warning_msg)
607+
Color.pl('{!} {O}Warning: %s{W}' % warning_msg)
608+
if self.view:
609+
self.view.add_log(f'Warning: {warning_msg}')
610+
611+
# Check if deauth interface is on target channel
612+
if deauth_channel != target_channel:
613+
warning_msg = f'Deauth interface {self.deauth_interface} is on channel {deauth_channel}, expected {target_channel}'
614+
log_warning('AttackWPA', warning_msg)
615+
Color.pl('{!} {O}Warning: %s{W}' % warning_msg)
616+
if self.view:
617+
self.view.add_log(f'Warning: {warning_msg}')
618+
619+
# Log success if both match
620+
if capture_channel == target_channel and deauth_channel == target_channel:
621+
Color.pl('{+} {G}Both interfaces verified on channel %d{W}' % target_channel)
622+
if self.view:
623+
self.view.add_log(f'Channel sync verified: both on channel {target_channel}')
624+
625+
except Exception as e:
626+
log_warning('AttackWPA', f'Failed to verify channel synchronization: {e}')
627+
Color.pl('{!} {O}Warning: Could not verify channel synchronization{W}')
628+
523629
def _deauth_dual(self, target):
524630
"""
525631
Send deauthentication packets using dedicated deauth interface.

0 commit comments

Comments
 (0)