├── requirements.txt ├── .gitignore ├── README.md └── best-channel.py /requirements.txt: -------------------------------------------------------------------------------- 1 | scapy 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | best-channel 2 | ----- 3 | 4 | Find the wifi channel with the least interference to give to your router for better connection speeds. 5 | 6 | Requires: python 2.7, python-scapy, a wireless card capable of injection 7 | 8 | 9 | Usage 10 | ----- 11 | 12 | Automatically find the most powerful interface then perform testing 13 | 14 | ``` shell 15 | python best-channel.py 16 | ``` 17 | 18 | Choose a wireless interface 19 | 20 | ``` shell 21 | python best-channel.py -i wlan0 22 | ``` 23 | 24 | License 25 | ------- 26 | 27 | Copyright (c) 2014, Dan McInerney 28 | All rights reserved. 29 | 30 | Redistribution and use in source and binary forms, with or without 31 | modification, are permitted provided that the following conditions are met: 32 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 33 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 34 | * Neither the name of Dan McInerney nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 35 | 36 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 37 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 38 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 39 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 40 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 41 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 42 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 43 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 45 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 | -------------------------------------------------------------------------------- /best-channel.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | 4 | import logging 5 | logging.getLogger("scapy.runtime").setLevel(logging.ERROR) # Shut up Scapy 6 | from scapy.all import * 7 | conf.verb = 0 # Scapy I thought I told you to shut up 8 | import os 9 | import sys 10 | import time 11 | from threading import Thread, Lock 12 | from subprocess import Popen, PIPE 13 | from signal import SIGINT, signal 14 | import argparse 15 | import socket 16 | import struct 17 | import fcntl 18 | 19 | # Console colors 20 | W = '\033[0m' # white (normal) 21 | R = '\033[31m' # red 22 | G = '\033[32m' # green 23 | O = '\033[33m' # orange 24 | B = '\033[34m' # blue 25 | P = '\033[35m' # purple 26 | C = '\033[36m' # cyan 27 | GR = '\033[37m' # gray 28 | T = '\033[93m' # tan 29 | 30 | channels = {1:{}, 6:{}, 11:{}} 31 | 32 | def parse_args(): 33 | #Create the arguments 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument("-i", "--interface", help="Choose monitor mode interface. By default script \ 36 | will find the most powerful interface and starts monitor mode on it. Example: -i mon5") 37 | return parser.parse_args() 38 | 39 | ######################################## 40 | # Begin interface info and manipulation 41 | ######################################## 42 | 43 | def get_mon_iface(args): 44 | global monitor_on 45 | monitors, interfaces = iwconfig() 46 | if args.interface: 47 | monitor_on = True 48 | return args.interface 49 | if len(monitors) > 0: 50 | monitor_on = True 51 | return monitors[0] 52 | else: 53 | # Start monitor mode on a wireless interface 54 | print '['+G+'*'+W+'] Finding the most powerful interface...' 55 | interface = get_iface(interfaces) 56 | monmode = start_mon_mode(interface) 57 | return monmode 58 | 59 | def iwconfig(): 60 | monitors = [] 61 | interfaces = {} 62 | proc = Popen(['iwconfig'], stdout=PIPE, stderr=DN) 63 | for line in proc.communicate()[0].split('\n'): 64 | if len(line) == 0: continue # Isn't an empty string 65 | if line[0] != ' ': # Doesn't start with space 66 | wired_search = re.search('eth[0-9]|em[0-9]|p[1-9]p[1-9]', line) 67 | if not wired_search: # Isn't wired 68 | iface = line[:line.find(' ')] # is the interface 69 | if 'Mode:Monitor' in line: 70 | monitors.append(iface) 71 | elif 'IEEE 802.11' in line: 72 | if "ESSID:\"" in line: 73 | interfaces[iface] = 1 74 | else: 75 | interfaces[iface] = 0 76 | return monitors, interfaces 77 | 78 | def get_iface(interfaces): 79 | scanned_aps = [] 80 | 81 | if len(interfaces) < 1: 82 | sys.exit('['+R+'-'+W+'] No wireless interfaces found, bring one up and try again') 83 | if len(interfaces) == 1: 84 | for interface in interfaces: 85 | return interface 86 | 87 | # Find most powerful interface 88 | for iface in interfaces: 89 | count = 0 90 | proc = Popen(['iwlist', iface, 'scan'], stdout=PIPE, stderr=DN) 91 | for line in proc.communicate()[0].split('\n'): 92 | if ' - Address:' in line: # first line in iwlist scan for a new AP 93 | count += 1 94 | scanned_aps.append((count, iface)) 95 | print '['+G+'+'+W+'] Networks discovered by '+G+iface+W+': '+T+str(count)+W 96 | try: 97 | interface = max(scanned_aps)[1] 98 | return interface 99 | except Exception as e: 100 | for iface in interfaces: 101 | interface = iface 102 | print '['+R+'-'+W+'] Minor error:',e 103 | print ' Starting monitor mode on '+G+interface+W 104 | return interface 105 | 106 | def start_mon_mode(interface): 107 | print '['+G+'+'+W+'] Starting monitor mode on '+G+interface+W 108 | try: 109 | os.system('/sbin/ifconfig %s down' % interface) 110 | os.system('/sbin/iwconfig %s mode monitor' % interface) 111 | os.system('/sbin/ifconfig %s up' % interface) 112 | return interface 113 | except Exception: 114 | sys.exit('['+R+'-'+W+'] Could not start monitor mode') 115 | raise #################### 116 | 117 | def remove_mon_iface(mon_iface): 118 | os.system('/sbin/ifconfig %s down' % mon_iface) 119 | os.system('/sbin/iwconfig %s mode managed' % mon_iface) 120 | os.system('/sbin/ifconfig %s up' % mon_iface) 121 | 122 | def mon_mac(mon_iface): 123 | ''' 124 | http://stackoverflow.com/questions/159137/getting-mac-address 125 | ''' 126 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 127 | info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', mon_iface[:15])) 128 | mac = ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] 129 | print '['+G+'*'+W+'] Monitor mode: '+G+mon_iface+W+' - '+O+mac+W 130 | return mac 131 | 132 | ######################################## 133 | # End of interface info and manipulation 134 | ######################################## 135 | 136 | def cb(pkt): 137 | ''' 138 | Look for dot11 packets that aren't to or from broadcast address, 139 | are type 1 or 2 (control, data), and append the addr1 and addr2 140 | to the list of deauth targets. 141 | ''' 142 | # We're adding the AP and channel to the deauth list at time of creation rather 143 | # than updating on the fly in order to avoid costly for loops that require a lock 144 | if pkt.haslayer(Dot11): 145 | if pkt.addr1 and pkt.addr2: 146 | 147 | # Check if it's added to our AP list 148 | if pkt.haslayer(Dot11Beacon) or pkt.haslayer(Dot11ProbeResp): 149 | APs(pkt) 150 | 151 | def APs(pkt): 152 | ssid = pkt[Dot11Elt].info 153 | bssid = pkt[Dot11].addr3 154 | try: 155 | chans = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] 156 | # http://stackoverflow.com/questions/10818661/scapy-retrieving-rssi-from-wifi-packets 157 | sig_str = -(256-ord(pkt.notdecoded[-4:-3])) 158 | # airoscapy 159 | ap_channel = str(ord(pkt[Dot11Elt:3].info)) 160 | if ap_channel in chans: 161 | if ap_channel in ['1','2','3']: 162 | # Set to {MAC address:avg power signal} 163 | if bssid in channels[1]: 164 | channels[1][bssid].append(sig_str) 165 | else: 166 | channels[1][bssid] = [sig_str] 167 | elif ap_channel in ['4','5','6','7','8']: 168 | # Set to {MAC address:avg power signal} 169 | if bssid in channels[6]: 170 | channels[6][bssid].append(sig_str) 171 | else: 172 | channels[6][bssid] = [sig_str] 173 | elif ap_channel in ['9', '10', '11']: 174 | # Set to {MAC address:avg power signal} 175 | if bssid in channels[11]: 176 | channels[11][bssid].append(sig_str) 177 | else: 178 | channels[11][bssid] = [sig_str] 179 | except Exception: 180 | raise 181 | 182 | def output(err, num_aps, chan_interference, best_channel): 183 | os.system('clear') 184 | if err: 185 | print err 186 | else: 187 | print '[Channels '+G+'1-3'+W+'] Number of APs: '+T+'%d' % num_aps[1], W+' | Interference level: '+R+'%d' % chan_interference[1], W 188 | print '[Channels '+G+'4-8'+W+'] Number of APs: '+T+'%d' % num_aps[6], W+' | Interference level: '+R+'%d' % chan_interference[6], W 189 | print '[Channels '+G+'9-11'+W+'] Number of APs: '+T+'%d' % num_aps[11], W+' | Interference level: '+R+'%d' % chan_interference[11], W 190 | print '['+G+'+'+W+'] Recommended channel: '+G+'%s' % best_channel, W 191 | 192 | def channel_hop(mon_iface): 193 | ''' 194 | First time it runs through the channels it stays on each channel for 5 seconds 195 | in order to populate the deauth list nicely. After that it goes as fast as it can 196 | ''' 197 | global monchannel 198 | 199 | channelNum = 0 200 | maxChan = 11 201 | err = None 202 | 203 | while 1: 204 | channelNum +=1 205 | if channelNum > maxChan: 206 | channelNum = 1 207 | monchannel = str(channelNum) 208 | 209 | try: 210 | proc = Popen(['iw', 'dev', mon_iface, 'set', 'channel', monchannel], stdout=DN, stderr=PIPE) 211 | except OSError: 212 | print '['+R+'-'+W+'] Could not execute "iw"' 213 | os.kill(os.getpid(),SIGINT) 214 | sys.exit(1) 215 | for line in proc.communicate()[1].split('\n'): 216 | if len(line) > 2: # iw dev shouldnt display output unless there's an error 217 | err = '['+R+'-'+W+'] Channel hopping failed: '+R+line+W 218 | 219 | # avg_pwr_per_ap = {1:{bssid:ap_pwr}, 6:[...], 11:[...]} 220 | avg_pwr_per_ap = get_ap_pwr() 221 | num_aps = get_num_aps(avg_pwr_per_ap) 222 | chan_interference = get_chan_interference(avg_pwr_per_ap) 223 | best_channel = get_best_channel(chan_interference) 224 | 225 | output(err, num_aps, chan_interference, best_channel) 226 | time.sleep(.5) 227 | 228 | def get_best_channel(chan_interference): 229 | ''' 230 | Get the channel with the least interference 231 | ''' 232 | if chan_interference[1] == chan_interference[6] == chan_interference[11]: 233 | return 'All the same' 234 | 235 | least = min([chan_interference[1], chan_interference[6], chan_interference[11]]) 236 | if least == chan_interference[1]: 237 | return '1' 238 | elif least == chan_interference[6]: 239 | return '6' 240 | elif least == chan_interference[11]: 241 | return '11' 242 | 243 | def get_chan_interference(avg_pwr_per_ap): 244 | ''' 245 | Add together all the BSSID's avg pwr levels 246 | ''' 247 | interference_val = {1:0, 6:0, 11:0} 248 | total_power = {1:[], 6:[], 11:[]} 249 | for chan in [1, 6, 11]: 250 | for bssid in avg_pwr_per_ap[chan]: 251 | total_power[chan].append(avg_pwr_per_ap[chan][bssid]) 252 | interference_val[chan] = -sum(total_power[chan]) 253 | 254 | return interference_val 255 | 256 | def get_ap_pwr(): 257 | ''' 258 | Returns a dict of nonoverlapping channels which contains 259 | a dict of each BSSID's average power 260 | ''' 261 | avg_pwr_per_ap = {1:{}, 6:{}, 11:{}} 262 | for chan in [1, 6, 11]: 263 | # channels[chan] = {'bssid':[avgpwr, avgpwr]} 264 | for bssid in channels[chan]: 265 | bssid_avg_pwr = float(sum(channels[chan][bssid]))/len(channels[chan][bssid]) if len(channels[chan][bssid]) > 0 else 0 266 | avg_pwr_per_ap[chan][bssid] = bssid_avg_pwr 267 | 268 | return avg_pwr_per_ap 269 | 270 | def get_num_aps(avg_pwr_per_ap): 271 | 272 | num_aps = {1:0, 6:0, 11:0} 273 | for chan in [1, 6, 11]: 274 | num_aps[chan] = len(avg_pwr_per_ap[chan]) 275 | return num_aps 276 | 277 | def stop(signal, frame): 278 | if monitor_on: 279 | sys.exit( 280 | '\n['+R+'!'+W+'] Closing... You will probably have to reconnect to your \ 281 | wireless network if you were on one prior to running this script') 282 | else: 283 | remove_mon_iface(mon_iface) 284 | sys.exit('\n['+R+'!'+W+'] Closing... You will probably have to reconnect to your \ 285 | wireless network if you were on one prior to running this script') 286 | 287 | if __name__ == "__main__": 288 | if os.geteuid(): 289 | sys.exit('['+R+'-'+W+'] Please run as root') 290 | args = parse_args() 291 | DN = open(os.devnull, 'w') 292 | monitor_on = None 293 | mon_iface = get_mon_iface(args) 294 | conf.iface = mon_iface 295 | mon_MAC = mon_mac(mon_iface) 296 | 297 | hop = Thread(target=channel_hop, args=(mon_iface,)) 298 | hop.daemon = True 299 | hop.start() 300 | 301 | signal(SIGINT, stop) 302 | 303 | try: 304 | sniff(iface=mon_iface, store=0, prn=cb) 305 | except Exception as msg: 306 | remove_mon_iface(mon_iface) 307 | print '\n['+R+'!'+W+'] Sniffing failed: %s' % str(msg) 308 | sys.exit(0) 309 | --------------------------------------------------------------------------------