├── .gitignore ├── HISTORY ├── LICENSE ├── MANIFEST ├── README.md ├── VERSION ├── hotspotd.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 3 | 4 | *.iml 5 | 6 | ## Directory-based project format: 7 | .idea/ 8 | # if you remove the above rule, at least ignore the following: 9 | 10 | # User-specific stuff: 11 | # .idea/workspace.xml 12 | # .idea/tasks.xml 13 | # .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | # .idea/dataSources.ids 17 | # .idea/dataSources.xml 18 | # .idea/sqlDataSources.xml 19 | # .idea/dynamic.xml 20 | # .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | # .idea/gradle.xml 24 | # .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | # .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.ipr 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | ### Python template 49 | # Byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | env/ 60 | build/ 61 | develop-eggs/ 62 | dist/ 63 | downloads/ 64 | eggs/ 65 | .eggs/ 66 | lib/ 67 | lib64/ 68 | parts/ 69 | sdist/ 70 | var/ 71 | *.egg-info/ 72 | .installed.cfg 73 | *.egg 74 | 75 | # PyInstaller 76 | # Usually these files are written by a python script from a template 77 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 78 | *.manifest 79 | *.spec 80 | 81 | # Installer logs 82 | pip-log.txt 83 | pip-delete-this-directory.txt 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .coverage 89 | .coverage.* 90 | .cache 91 | nosetests.xml 92 | coverage.xml 93 | *,cover 94 | 95 | # Translations 96 | *.mo 97 | *.pot 98 | 99 | # Django stuff: 100 | *.log 101 | 102 | # Sphinx documentation 103 | docs/_build/ 104 | 105 | # PyBuilder 106 | target/ 107 | 108 | # Configuration files 109 | *.conf 110 | *.json 111 | 112 | # PyEnv 113 | .python-version 114 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | == Hotspotd 0.2.5 == 2 | 3 | 1. Config path option support 4 | 2. Start/Stop command execution bugfixes 5 | 6 | == Hotspotd 0.2.4 == 7 | 8 | 1. Hidden SSID support 9 | 2. Some bugfixes 10 | 11 | == Hotspotd 0.2.3 == 12 | 13 | 1. Support for OPEN auth for acces point by default 14 | 2. Template file removed, config embedded to source code 15 | 3. Now using logging insted of print where possible 16 | 17 | == Hotspotd 0.2.2 == 18 | 19 | 1. Program execution on start/stop addedd 20 | 2. Some bugfixes 21 | 22 | == Hotspotd 0.2.1 == 23 | 24 | 1. Channel setting support 25 | 2. Some bugfixes 26 | 27 | == Hotspotd 0.2.0 == 28 | 29 | 1. Rewritten to use Click library 30 | 2. All code moved to hotspotd.py file 31 | 3. Interfaces automatic selection algorithm changed 32 | 4. MAC address changing support 33 | 5. New configuration file location /etc/hotspotd.json 34 | 6. setup.py rewritten from scratch to use setuptools instead of distutis 35 | 36 | 37 | == Hotspotd 0.1.7 == 38 | 39 | 1. Added HISTORY and VERSION files for tracking. 40 | 2. Changed labelling on startup screen. 41 | 3. Added .gitignore file. 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Prahlad Yeri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | __init__.py 3 | cli.py 4 | hotspotd 5 | main.py 6 | run.dat 7 | setup.py 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Introduction 2 | *Hotspotd* is a small daemon to create a wifi hotspot on linux. It depends on *hostapd* for AP provisioning and *dnsmasq* to assign IP addresses to devices. 3 | 4 | Hotspotd works by creating a virtual NAT (Network address transation) table between your connected device and the internet using linux *iptables*. 5 | 6 | #Installation 7 | To install hotspotd, just follow these steps: 8 | ``` 9 | git clone https://github.com/0x90/hotspotd/ 10 | cd hotspotd 11 | pip install -e . 12 | ``` 13 | 14 | .. or use the short way 15 | ``` 16 | pip install -e git+https://github.com/0x90/hotspotd 17 | ``` 18 | 19 | To uninstall hotspotd, just say: 20 | 21 | ```sudo python setup.py uninstall``` 22 | 23 | #Dependencies 24 | * *dnsmasq* (typically pre-installed on most linux distributions) 25 | * *hostapd* for AP provisioning 26 | 27 | To install hostapd on ubuntu: 28 | 29 | ```apt-get install hostapd``` 30 | 31 | Or on RHEL based distros: 32 | 33 | ```yum install hostapd``` 34 | 35 | Arch Linux based distros: 36 | ``` 37 | pacman -Sy hostapd dnsmasq 38 | ``` 39 | 40 | #Usage 41 | 42 | To start hotspot: 43 | 44 | ```sudo hotspotd start``` 45 | 46 | To stop hotspot: 47 | 48 | ```sudo hotspotd stop``` 49 | 50 | The first time you run hotspotd, it will ask you for configuration values for SSID, password, etc. Alternatively, you may also run: 51 | 52 | ```sudo hotspotd configure``` 53 | 54 | #Troubleshooting 55 | 56 | * Make sure all dependencies (hostapd, dnsmasq and python 2.7) are installed. 57 | 58 | * hotspotd creates the NAT by manipulating iptables rules. So if you have any other firewall software that manipulates the iptables rules (such as the firewalld on fedora), make sure you disable that. 59 | 60 | 61 | * To create a hotspot, your wifi must support AP mode. To find that out, use this process: 62 | 63 | * Find your kernel driver module in use by issuing the below command: 64 | 65 | ```lspci -k | grep -A 3 -i network``` 66 | 67 | (example module: ath9k) 68 | 69 | * Now, use the below command to find out your wifi capabilities (replace ath9k by your kernel driver): 70 | 71 | ```modinfo ath9k | grep depend``` 72 | 73 | * If the above output includes “mac80211” then it means your wifi card will support the AP mode. 74 | 75 | #Testing status 76 | This package has been tested on Qualcomm Atheros adapter on the following distros: 77 | 78 | * Ubuntu 12.04 LTS 79 | * Ubuntu 14.04 LTS 80 | * Arch Linux 81 | 82 | In theory, it should work with all other distros too (on machines having wifi adapters supported by hostapd), but you will have to try that out and tell me! 83 | 84 | #Notes 85 | * Replace `sudo` with `su` or `su -c` if you manage superuser access in that manner. 86 | * PyPI home page could be found at https://pypi.python.org/pypi/hotspotd. 87 | * I need someone to test this daemon across various linux distros. If you are interested in testing of open-source apps, please contact me. 88 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.5 2 | -------------------------------------------------------------------------------- /hotspotd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # @authors: Prahlad Yeri, Oleg Kupreev 3 | # @description: Small script to create a wifi hotspot on linux 4 | # @license: MIT 5 | 6 | import array 7 | import fcntl 8 | import glob 9 | import json 10 | import logging 11 | import os 12 | import socket 13 | import struct 14 | import subprocess 15 | import sys 16 | import time 17 | import re 18 | import click 19 | 20 | __license__ = 'MIT' 21 | __version__ = '0.3.0' 22 | 23 | WPA2_CONFIG = """ 24 | interface=%s 25 | driver=nl80211 26 | ssid=%s 27 | hw_mode=g 28 | channel=%i 29 | macaddr_acl=0 30 | ignore_broadcast_ssid=%i 31 | auth_algs=1 32 | wpa=2 33 | wpa_passphrase=%s 34 | wpa_key_mgmt=WPA-PSK 35 | rsn_pairwise=CCMP 36 | ieee80211d=1 37 | country_code=RU 38 | ieee80211n=1 39 | wmm_enabled=1 40 | """ 41 | 42 | 43 | OPEN_CONFIG = """ 44 | interface=%s 45 | ssid=%s 46 | hw_mode=g 47 | channel=%i 48 | auth_algs=1 49 | wmm_enabled=1 50 | """ 51 | 52 | HOSTAPD_CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'run.conf') 53 | 54 | class Hotspotd(object): 55 | 56 | def __init__(self, 57 | wlan=None, inet=None, 58 | ip='192.168.45.1', netmask='255.255.255.0', mac='00:de:ad:be:ef:00', 59 | channel=6, ssid='hotspod', password='12345678', hidden=False, 60 | start_exec=None, stop_exec=None, 61 | verbose=False): 62 | 63 | # Network params 64 | self.wlan = str(wlan) 65 | self.inet = str(inet) 66 | self.ip = ip 67 | self.netmask = netmask 68 | 69 | # AP params 70 | self.mac = mac 71 | self.channel = int(channel) 72 | self.ssid = ssid 73 | self.password = password 74 | self.hidden = hidden 75 | 76 | # Exec params 77 | self.start_exec = start_exec 78 | self.stop_exec = stop_exec 79 | 80 | # Config files 81 | self.config_files = {'hotspotd': '/etc/hotspotd.json', 82 | # TODO: move config to separate folder? 83 | 'hostapd': HOSTAPD_CONFIG_PATH} 84 | 85 | # Initialize logger 86 | self.logger = logging.getLogger(__name__) 87 | self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) 88 | handler = logging.StreamHandler() 89 | handler.setFormatter( 90 | logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%d.%m.%Y %H:%M:%S')) 91 | self.logger.addHandler(handler) 92 | 93 | # Show current configuration files path 94 | self.logger.info('Config files:') 95 | for k in self.config_files.keys(): 96 | self.logger.info('\t%s\t%s' % (k, self.config_files[k])) 97 | 98 | def execute(self, command='', errorstring='', wait=True, shellexec=False, ags=None): 99 | try: 100 | if shellexec: 101 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 102 | self.logger.debug('command: ' + command) 103 | else: 104 | p = subprocess.Popen(args=ags) 105 | self.logger.debug('command: ' + ags[0]) 106 | 107 | if wait: 108 | p.wait() 109 | result = get_stdout(p) 110 | return result 111 | else: 112 | self.logger.debug('not waiting') 113 | return p 114 | except subprocess.CalledProcessError: 115 | self.logger.error('Subprocess error occured:' + errorstring) 116 | return errorstring 117 | except Exception as ex: 118 | self.logger.error('Exception occured: %s' % ex) 119 | return errorstring 120 | 121 | def execute_shell(self, command, error=''): 122 | self.logger.info('CMD: ' + command) 123 | return self.execute(command, wait=True, shellexec=True, errorstring=error) 124 | 125 | def is_process_running(self, name): 126 | s = self.execute_shell('ps aux |grep ' + name + ' |grep -v grep') 127 | return 0 if len(s) == 0 else int(s.split()[1]) 128 | 129 | def get_sysctl(self, setting): 130 | result = self.execute_shell('sysctl ' + setting) 131 | return result.split('=')[1].lstrip() if '=' in result else result 132 | 133 | def set_sysctl(self, setting, value): 134 | return self.execute_shell('sysctl -w ' + setting + '=' + value) 135 | 136 | def set_mac(self): 137 | """ Set the device's mac address. Device must be down for this to succeed. """ 138 | if self.mac is None: 139 | self.logger.info('No MAC address to set') 140 | return 141 | 142 | self.logger.info('Setting interface %s MAC address to %s' % (self.wlan, self.mac)) 143 | try: 144 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 145 | macbytes = [int(i, 16) for i in self.mac.split(':')] 146 | ifreq = struct.pack('16sH6B8x', str(self.wlan), socket.AF_UNIX, *macbytes) 147 | fcntl.ioctl(s.fileno(), SIOCSIFHWADDR, ifreq) 148 | s.close() 149 | except Exception as ex: 150 | self.logger.error('MAC address setup error %s' % ex) 151 | 152 | def set_channel(self): 153 | self.logger.info('Set %s channel %i' % (self.wlan, self.channel)) 154 | try: 155 | st = struct.pack('16sihbb', str(self.wlan), self.channel, 0, 0, 0) 156 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 157 | fcntl.ioctl(s.fileno(), SIOCSIWFREQ, st) 158 | s.close() 159 | except Exception as ex: 160 | self.logger.error('Channel setup error %s' % ex) 161 | 162 | def generate_hostapd_config(self, config_path=HOSTAPD_CONFIG_PATH): 163 | # Generate text 164 | if self.password == '': 165 | # OPN 166 | text = OPEN_CONFIG % \ 167 | (self.wlan, self.ssid, self.channel) 168 | else: 169 | # WPA2/PSK 170 | text = WPA2_CONFIG % \ 171 | (self.wlan, self.ssid, self.channel, 1 if self.hidden else 0, self.password) 172 | 173 | # Save hostapd conf file 174 | # with open(self.config_files['hostapd'], 'w') as f: 175 | with open(config_path, 'w') as f: 176 | f.write(text) 177 | self.logger.info('created hostapd configuration: %s' % config_path) 178 | 179 | def start(self, free=False): 180 | # Check WiFi interfaces existence 181 | if self.wlan not in get_ifaces_names(True): 182 | click.secho("Wireless interface %s NOT found" % self.wlan, fg='red') 183 | return 184 | 185 | # Check Internet interface existence 186 | if self.inet not in get_ifaces_names(): 187 | click.secho("Internet interface %s NOT found" % self.inet, fg='red') 188 | return 189 | 190 | # Try to free wireless 191 | if free: 192 | # ATTENTION!!! STOP ALL WIRELESS INTERFACES 193 | try: 194 | result = self.execute_shell('nmcli radio wifi off') 195 | if "error" in result.lower(): 196 | self.execute_shell('nmcli nm wifi off') 197 | self.execute_shell('rfkill unblock wlan') 198 | time.sleep(1) 199 | self.logger.info('done.') 200 | except Exception as ex: 201 | self.logger.error('Error caught while freeing wireless %s' % ex) 202 | 203 | # Prepare hostapd configuration file if required 204 | # if os.path.exists(self.config_files['hostapd']): 205 | # TODO: ask if config overwrite is needed 206 | self.generate_hostapd_config() 207 | 208 | # Prepare interface 209 | self.logger.info('using interface: %s on IP: %s MAC: %s' % (self.wlan, self.ip, self.mac)) 210 | self.execute_shell('ifconfig ' + self.wlan + ' down') 211 | self.set_mac() 212 | self.execute_shell('ifconfig %s up %s netmask %s' % (self.wlan, self.ip, self.netmask)) 213 | time.sleep(2) 214 | 215 | # Split IP to parts 216 | i = self.ip.rindex('.') 217 | ipparts = self.ip[0:i] 218 | 219 | # stop dnsmasq if already running. 220 | if self.is_process_running('dnsmasq') > 0: 221 | self.logger.info('stopping dnsmasq') 222 | self.execute_shell('killall dnsmasq') 223 | 224 | # stop hostapd if already running. 225 | if self.is_process_running('hostapd') > 0: 226 | self.logger.info('stopping hostapd') 227 | self.execute_shell('killall -9 hostapd') 228 | 229 | # enable forwarding in sysctl. 230 | self.logger.info('enabling forward in sysctl.') 231 | self.set_sysctl('net.ipv4.ip_forward', '1') 232 | 233 | # enable forwarding in iptables. 234 | self.logger.info('creating NAT using iptables: %s <--> %s' % (self.wlan, self.inet)) 235 | self.execute_shell('iptables -P FORWARD ACCEPT') 236 | 237 | # add iptables rules to create the NAT. 238 | self.execute_shell('iptables --table nat --delete-chain') 239 | self.execute_shell('iptables --table nat -F') 240 | self.execute_shell('iptables --table nat -X') 241 | self.execute_shell('iptables -t nat -A POSTROUTING -o %s -j MASQUERADE' % self.inet) 242 | self.execute_shell( 243 | 'iptables -A FORWARD -i %s -o %s -j ACCEPT -m state --state RELATED,ESTABLISHED' % (self.inet, self.wlan)) 244 | self.execute_shell('iptables -A FORWARD -i %s -o %s -j ACCEPT' % (self.wlan, self.inet)) 245 | 246 | # allow traffic to/from wlan 247 | self.execute_shell('iptables -A OUTPUT --out-interface %s -j ACCEPT' % self.inet) 248 | self.execute_shell('iptables -A INPUT --in-interface %s -j ACCEPT' % self.wlan) 249 | 250 | # start dnsmasq 251 | s = 'dnsmasq --dhcp-authoritative --interface=%s --dhcp-range=%s.20,%s.100,%s,4h' % \ 252 | (self.wlan, ipparts, ipparts, self.netmask) 253 | self.logger.info('running dnsmasq: %s' % s) 254 | self.execute_shell(s) 255 | 256 | # start hostapd daemon 257 | s = 'hostapd -B %s' % self.config_files['hostapd'] 258 | self.logger.info(s) 259 | time.sleep(2) 260 | self.execute_shell(s) 261 | 262 | # Execute 263 | if self.start_exec != '': 264 | self.logger.info('Executing: %s' % self.start_exec) 265 | subprocess.Popen(self.start_exec, shell=True, stdout=subprocess.PIPE) 266 | 267 | self.logger.info('hotspot is running.') 268 | 269 | def stop(self): 270 | # bring down the interface 271 | self.execute_shell('ifconfig ' + self.wlan + ' down') 272 | 273 | # stop hostapd 274 | if self.is_process_running('hostapd') > 0: 275 | self.logger.info('stopping hostapd') 276 | self.execute_shell('killall -9 hostapd') 277 | 278 | # stop dnsmasq 279 | if self.is_process_running('dnsmasq') > 0: 280 | self.logger.info('stopping dnsmasq') 281 | self.execute_shell('killall dnsmasq') 282 | 283 | # disable forwarding in iptables. 284 | self.logger.info('disabling forward rules in iptables.') 285 | self.execute_shell('iptables -P FORWARD DROP') 286 | 287 | # delete iptables rules that were added for wlan traffic. 288 | self.execute_shell('iptables -D OUTPUT --out-interface ' + self.wlan + ' -j ACCEPT') 289 | self.execute_shell('iptables -D INPUT --in-interface ' + self.wlan + ' -j ACCEPT') 290 | self.execute_shell('iptables --table nat --delete-chain') 291 | self.execute_shell('iptables --table nat -F') 292 | self.execute_shell('iptables --table nat -X') 293 | 294 | # disable forwarding in sysctl. 295 | self.logger.info('disabling forward in sysctl.') 296 | self.set_sysctl('net.ipv4.ip_forward', '0') 297 | 298 | # Execute 299 | if self.stop_exec != '': 300 | self.logger.info('Executing: %s' % self.stop_exec) 301 | subprocess.Popen(self.stop_exec, shell=True, stdout=subprocess.PIPE) 302 | 303 | self.logger.info('hotspot has stopped.') 304 | 305 | def save(self, filename=None): 306 | fname = self.config_files['hotspotd'] if filename is None else filename 307 | 308 | dc = {'wlan': self.wlan, 'inet': self.inet, 'ip': self.ip, 'netmask': self.netmask, 'mac': self.mac, 309 | 'channel': self.channel, 310 | 'ssid': self.ssid, 'password': self.password, 'hidden': self.hidden, 311 | 'start_exec': self.start_exec, 'stop_exec': self.stop_exec} 312 | json.dump(dc, open(fname, 'wb')) 313 | 314 | self.logger.info('Configuration saved to %s. Run "hotspotd start" to start the router.' % fname) 315 | 316 | def load(self, filename=None): 317 | # Read configuration file 318 | fname = self.config_files['hotspotd'] if filename is None else filename 319 | self.logger.info('Loading configuration from %s' % fname) 320 | dc = json.load(open(fname, 'rb')) 321 | 322 | # Load variables 323 | self.wlan = dc['wlan'] 324 | self.inet = dc['inet'] 325 | self.ip = dc['ip'] if 'ip' in dc else '192.168.45.1' 326 | self.netmask = dc['netmask'] if 'netmask' in dc else '255.255.255.0' 327 | self.mac = dc['mac'] if 'mac' in dc else None 328 | self.channel = dc['channel'] if 'channel' in dc else 6 329 | self.ssid = dc['ssid'] if 'ssid' in dc else 'hotspotd' 330 | self.password = dc['password'] if 'password' in dc else '' 331 | self.hidden = dc['hidden'] if 'hidden' in dc else False 332 | self.start_exec = dc['start_exec'] if 'start_exec' in dc else None 333 | self.stop_exec = dc['stop_exec'] if 'stop_exec' in dc else None 334 | 335 | 336 | def get_stdout(pi): 337 | result = pi.communicate() 338 | return result[0] if len(result[0]) > 0 else result[1] 339 | 340 | 341 | # From linux/sockios.h 342 | SIOCGIFCONF = 0x8912 343 | SIOCGIFINDEX = 0x8933 344 | SIOCGIFFLAGS = 0x8913 345 | SIOCSIFFLAGS = 0x8914 346 | SIOCGIFHWADDR = 0x8927 347 | SIOCSIFHWADDR = 0x8924 348 | SIOCGIFADDR = 0x8915 349 | SIOCSIFADDR = 0x8916 350 | SIOCGIFNETMASK = 0x891B 351 | SIOCSIFNETMASK = 0x891C 352 | SIOCETHTOOL = 0x8946 353 | # ioctl calls for the Linux/i386 kernel 354 | SIOCSIWCOMMIT = 0x8B00 # Commit pending changes to driver 355 | SIOCGIWNAME = 0x8B01 # get name == wireless protocol 356 | SIOCSIWNWID = 0x8B02 # set network id (pre-802.11) 357 | SIOCGIWNWID = 0x8B03 # get network id (the cell) 358 | SIOCSIWFREQ = 0x8B04 # set channel/frequency 359 | SIOCGIWFREQ = 0x8B05 # get channel/frequency 360 | SIOCSIWMODE = 0x8B06 # set the operation mode 361 | SIOCGIWMODE = 0x8B07 # get operation mode 362 | SIOCSIWSENS = 0x8B08 # set sensitivity (dBm) 363 | SIOCGIWSENS = 0x8B09 # get sensitivity 364 | SIOCSIWRANGE = 0x8B0A # Unused 365 | SIOCGIWRANGE = 0x8B0B # Get range of parameters 366 | SIOCSIWPRIV = 0x8B0C # Unused 367 | SIOCGIWPRIV = 0x8B0D # get private ioctl interface info 368 | SIOCSIWSTATS = 0x8B0E # Unused 369 | SIOCGIWSTATS = 0x8B0F # Get /proc/net/wireless stats 370 | SIOCSIWSPY = 0x8B10 # set spy addresses 371 | SIOCGIWSPY = 0x8B11 # get spy info (quality of link) 372 | SIOCSIWTHRSPY = 0x8B12 # set spy threshold (spy event) 373 | SIOCGIWTHRSPY = 0x8B13 # get spy threshold 374 | SIOCSIWAP = 0x8B14 # set AP MAC address 375 | SIOCGIWAP = 0x8B15 # get AP MAC addresss 376 | SIOCGIWAPLIST = 0x8B17 # Deprecated in favor of scanning 377 | SIOCSIWSCAN = 0x8B18 # set scanning off 378 | SIOCGIWSCAN = 0x8B19 # get scanning results 379 | SIOCSIWESSID = 0x8B1A # set essid 380 | SIOCGIWESSID = 0x8B1B # get essid 381 | SIOCSIWNICKN = 0x8B1C # set node name/nickname 382 | SIOCGIWNICKN = 0x8B1D # get node name/nickname 383 | SIOCSIWRATE = 0x8B20 # set default bit rate (bps) 384 | SIOCGIWRATE = 0x8B21 # get default bit rate (bps) 385 | SIOCSIWRTS = 0x8B22 # set RTS/CTS threshold (bytes) 386 | SIOCGIWRTS = 0x8B23 # get RTS/CTS threshold (bytes) 387 | SIOCSIWFRAG = 0x8B24 # set fragmentation thr (bytes) 388 | SIOCGIWFRAG = 0x8B25 # get fragmentation thr (bytes) 389 | SIOCSIWTXPOW = 0x8B26 # set transmit power (dBm) 390 | SIOCGIWTXPOW = 0x8B27 # get transmit power (dBm) 391 | SIOCSIWRETRY = 0x8B28 # set retry limits and lifetime 392 | SIOCGIWRETRY = 0x8B29 # get retry limits and lifetime 393 | SIOCSIWENCODE = 0x8B2A # set encryption information 394 | SIOCGIWENCODE = 0x8B2B # get encryption information 395 | SIOCSIWPOWER = 0x8B2C # set Power Management settings 396 | SIOCGIWPOWER = 0x8B2D # get power managment settings 397 | SIOCSIWMODUL = 0x8B2E # set Modulations settings 398 | SIOCGIWMODUL = 0x8B2F # get Modulations settings 399 | SIOCSIWGENIE = 0x8B30 # set generic IE 400 | SIOCGIWGENIE = 0x8B31 # get generic IE 401 | # WPA 402 | SIOCSIWMLME = 0x8B16 # request MLME operation; uses struct iw_mlme 403 | SIOCSIWAUTH = 0x8B32 # set authentication mode params 404 | SIOCGIWAUTH = 0x8B33 # get authentication mode params 405 | SIOCSIWENCODEEXT = 0x8B34 # set encoding token & mode 406 | SIOCGIWENCODEEXT = 0x8B35 # get encoding token & mode 407 | SIOCSIWPMKSA = 0x8B36 # PMKSA cache operation 408 | 409 | SIOCIWFIRST = 0x8B00 # FIRST ioctl identifier 410 | SIOCIWLAST = 0x8BFF # LAST ioctl identifier 411 | 412 | 413 | def get_interfaces_dict(): 414 | is_64bits = sys.maxsize > 2 ** 32 415 | struct_size = 40 if is_64bits else 32 416 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 417 | max_possible = 8 # initial value 418 | # names = '' 419 | # outbytes = 0 420 | while True: 421 | _bytes = max_possible * struct_size 422 | names = array.array('B') 423 | for i in range(0, _bytes): 424 | names.append(0) 425 | outbytes = struct.unpack('iL', fcntl.ioctl( 426 | s.fileno(), 427 | SIOCGIFCONF, 428 | struct.pack('iL', _bytes, names.buffer_info()[0]) 429 | ))[0] 430 | if outbytes == _bytes: 431 | max_possible *= 2 432 | else: 433 | break 434 | namestr = names.tostring() 435 | ifaces = {} 436 | for i in range(0, outbytes, struct_size): 437 | iface_name = bytes.decode(namestr[i:i + 16]).split('\0', 1)[0] 438 | iface_addr = socket.inet_ntoa(namestr[i + 20:i + 24]) 439 | ifaces[iface_name] = iface_addr 440 | return ifaces 441 | 442 | 443 | def get_iface_list(): 444 | return [x for (x, y) in get_interfaces_dict().items()] 445 | 446 | 447 | def get_auto_wifi_interface(): 448 | wifi_interfaces = get_ifaces_names(True) 449 | net_interfaces = map(lambda (x, y): x, get_interfaces_dict().items()) 450 | for wifi in wifi_interfaces: 451 | if wifi not in net_interfaces: 452 | return str(wifi) 453 | 454 | return None 455 | 456 | 457 | def get_default_iface(): 458 | route = "/proc/net/route" 459 | with open(route) as f: 460 | for line in f.readlines(): 461 | try: 462 | iface, dest, _, flags, _, _, _, _, _, _, _, = line.strip().split() 463 | if dest != '00000000' or not int(flags, 16) & 2: 464 | continue 465 | return iface 466 | except Exception as ex: 467 | # TODO: add error handling 468 | continue 469 | 470 | return None 471 | 472 | 473 | def get_ifaces_names(wireless=False): 474 | return [f.split('/')[-2] for f in glob.glob("/sys/class/net/*/phy80211")] if wireless \ 475 | else os.listdir('/sys/class/net') 476 | 477 | 478 | def get_interface_mac(ifname): 479 | if ifname is None: 480 | return None 481 | 482 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 483 | info = fcntl.ioctl(s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname[:15])) 484 | s.close() 485 | return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] 486 | 487 | 488 | @click.group() 489 | @click.option('-C', '--config', help='Config file location', type=click.Path(), default='/etc/hotspotd.json') 490 | @click.option('--debug', help='Enable debug output', is_flag=True) 491 | @click.pass_context 492 | def cli(ctx, config, debug): 493 | ctx.obj = {} 494 | if os.geteuid() != 0: 495 | click.secho("You need root permissions to do this, sloth!", fg='red') 496 | sys.exit(1) 497 | 498 | ctx.obj['DEBUG'] = debug 499 | ctx.obj['CONFIG'] = config 500 | 501 | 502 | def validate_ip(ctx, param, value): 503 | try: 504 | socket.inet_aton(value) 505 | return value 506 | except socket.error: 507 | raise click.BadParameter('Non valid IP address') 508 | 509 | 510 | def validate_inet(ctx, param, value): 511 | if value not in get_iface_list(): 512 | raise click.BadParameter('Non valid inet interface') 513 | return value 514 | 515 | 516 | def validate_wlan(ctx, param, value): 517 | if value not in get_ifaces_names(True): 518 | raise click.BadParameter('Non valid wireless interface') 519 | return value 520 | 521 | 522 | def validate_password(ctx, param, value): 523 | if 0 > len(value) < 8: 524 | raise click.BadParameter('WiFi password must be 8 chars length minimum') 525 | return value 526 | 527 | 528 | def validate_mac(ctx, param, value): 529 | if not re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", value.lower()): 530 | raise click.BadParameter('Non valid MAC address') 531 | return value.lower() 532 | 533 | 534 | def validate_channel(ctx, param, value): 535 | if not isinstance(value, int): 536 | raise click.BadParameter('Non valid WiFi channel. Should be integer') 537 | 538 | ival = int(value) 539 | if 0 < ival < 15: 540 | return ival 541 | 542 | raise click.BadParameter('Non valid WiFi channel. Should be > 0 and < 15') 543 | 544 | 545 | # def validate_exec(ctx, param, value): 546 | # TODO: add validation 547 | # return value 548 | 549 | 550 | @cli.command() 551 | @click.option('-W', '--wlan', prompt='WiFi interface to use for AP', callback=validate_wlan, 552 | default=get_auto_wifi_interface()) 553 | @click.option('-I', '--inet', prompt='Network interface connected to Internet', callback=validate_inet, 554 | default=get_default_iface()) 555 | @click.option('-i', '--ip', prompt='Access point IP address', callback=validate_ip, default='192.168.45.1') 556 | @click.option('-n', '--netmask', prompt='Netmask for network', callback=validate_ip, default='255.255.255.0') 557 | @click.option('-m', '--mac', prompt='WiFi interface MAC address', callback=validate_mac, 558 | default=get_interface_mac(get_auto_wifi_interface())) 559 | @click.option('-c', '--channel', prompt='WiFi channel to use for AP', default=6, type=int, callback=validate_channel) 560 | @click.option('-s', '--ssid', prompt='WiFi access point SSID', default='MosMetro_Free') 561 | @click.option('-p', '--password', prompt='WiFi password', hide_input=True, confirmation_prompt=True, 562 | callback=validate_password, default='') 563 | @click.option('-H', '--hidden', is_flag=True, prompt='Hidden SSID') 564 | @click.option('--start-exec', prompt='execute something on start', default='') 565 | @click.option('--stop-exec', prompt='execute something on stop', default='') 566 | @click.pass_context 567 | def configure(ctx, wlan, inet, ip, netmask, mac, channel, ssid, password, hidden, start_exec, stop_exec): 568 | """Configure Hotspotd""" 569 | h = Hotspotd(wlan, inet, ip, netmask, mac, channel, ssid, password, hidden, start_exec, stop_exec) 570 | h.save(ctx.obj['CONFIG']) 571 | 572 | 573 | @cli.command() 574 | @click.pass_context 575 | def start(ctx): 576 | """Start hotspotd""" 577 | # TODO: add running not in background 578 | h = Hotspotd() 579 | h.load(ctx.obj['CONFIG']) 580 | h.start() 581 | 582 | 583 | @cli.command() 584 | @click.pass_context 585 | def stop(ctx): 586 | """Stop Hotspotd""" 587 | h = Hotspotd() 588 | h.load(ctx.obj['CONFIG']) 589 | h.stop() 590 | 591 | 592 | def check_sysfile(filename): 593 | if os.path.exists('/usr/sbin/' + filename): 594 | return '/usr/sbin/' + filename 595 | elif os.path.exists('/sbin/' + filename): 596 | return '/sbin/' + filename 597 | else: 598 | return '' 599 | 600 | 601 | @cli.command() 602 | @click.option('-o', '--output', default=HOSTAPD_CONFIG_PATH, help='Hostapd config file path') 603 | @click.pass_context 604 | def generate(ctx, output): 605 | """Generate hostapd configuration file""" 606 | h = Hotspotd() 607 | h.load(ctx.obj['CONFIG']) 608 | h.generate_hostapd_config(output) 609 | 610 | 611 | @cli.command() 612 | @click.pass_context 613 | def check(ctx): 614 | """Check dependencies: hostapd, dsmasq""" 615 | satisfied = True 616 | 617 | if len(check_sysfile('hostapd')) == 0: 618 | click.secho('hostapd executable not found. Make sure you have installed hostapd.', fg='red') 619 | satisfied = False 620 | 621 | if len(check_sysfile('dnsmasq')) == 0: 622 | click.secho('dnsmasq executable not found. Make sure you have installed dnsmasq.', fg='red') 623 | satisfied = False 624 | 625 | # TODO: add dependencies installation 626 | if satisfied: 627 | click.secho('All dependencies found 8).', fg='green') 628 | 629 | 630 | if __name__ == '__main__': 631 | cli(obj={}) 632 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # @authors: Prahlad Yeri, Oleg Kupreev 3 | # @description: Small daemon to create a WiFi hotspot on Linux 4 | # @license: MIT 5 | from setuptools import setup 6 | 7 | setup( 8 | name='hotspotd', 9 | license='MIT', 10 | author='Prahlad Yeri, Oleg Kupreev', 11 | version='0.2.5', 12 | description='Small daemon to create a wifi hotspot on Linux', 13 | py_modules=['hotspotd'], 14 | install_requires=[ 15 | 'Click', 16 | ], 17 | entry_points=''' 18 | [console_scripts] 19 | hotspotd=hotspotd:cli 20 | ''', 21 | ) 22 | --------------------------------------------------------------------------------