Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

[![License Badge](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Compatibility](https://img.shields.io/badge/python-3-brightgreen.svg)](PROJECT)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
[![HitCount](http://hits.dwyl.io/bitbrute/evillimiter.svg)](http://hits.dwyl.io/bitbrute/evillimiter)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-occasionally-orange.svg)](https://github.com/bitbrute/evillimiter/graphs/commit-activity)
[![HitCount](http://hits.dwyl.com/bitbrute/evillimiter.svg)](http://hits.dwyl.io/bitbrute/evillimiter)
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.svg?v=102)](https://github.com/ellerbrock/open-source-badge/)

A tool to monitor, analyze and limit the bandwidth (upload/download) of devices on your local network without physical or administrative access.<br>
Expand Down Expand Up @@ -77,5 +77,5 @@ Type ```evillimiter``` or ```python3 bin/evillimiter``` to run the tool.

## License

Copyright (c) 2019 by [bitbrute](https://github.com/bitbrute). Some rights reserved.<br>
[Evil Limiter](https://github.com/bitbrute/evillimiter) is licensed under the MIT License as stated in the [LICENSE file](LICENSE).
Copyright (c) 2026 by [bitbrute](https://github.com/bitbrute). Some rights reserved.<br>
[Evil Limiter](https://github.com/bitbrute/evillimiter) is licensed under the MIT License as stated in the [LICENSE file](LICENSE).
71 changes: 61 additions & 10 deletions evillimiter/menus/main_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from evillimiter.networking.host import Host
from evillimiter.networking.limit import Limiter, Direction
from evillimiter.networking.spoof import ARPSpoofer
from evillimiter.networking.scan import HostScanner
from evillimiter.networking.scan import HostScanner, ScanIntensity
from evillimiter.networking.monitor import BandwidthMonitor
from evillimiter.networking.watch import HostWatcher

Expand All @@ -31,6 +31,7 @@ def __init__(self, version, interface, gateway_ip, gateway_mac, netmask):

scan_parser = self.parser.add_subparser('scan', self._scan_handler)
scan_parser.add_parameterized_flag('--range', 'iprange')
scan_parser.add_parameterized_flag('--intensity', 'intensity')

limit_parser = self.parser.add_subparser('limit', self._limit_handler)
limit_parser.add_parameter('id')
Expand All @@ -51,6 +52,7 @@ def __init__(self, version, interface, gateway_ip, gateway_mac, netmask):
add_parser.add_parameterized_flag('--mac', 'mac')

monitor_parser = self.parser.add_subparser('monitor', self._monitor_handler)
monitor_parser.add_parameter('id')
monitor_parser.add_parameterized_flag('--interval', 'interval')

analyze_parser = self.parser.add_subparser('analyze', self._analyze_handler)
Expand Down Expand Up @@ -85,7 +87,7 @@ def __init__(self, version, interface, gateway_ip, gateway_mac, netmask):
self.arp_spoofer = ARPSpoofer(self.interface, self.gateway_ip, self.gateway_mac)
self.limiter = Limiter(self.interface)
self.bandwidth_monitor = BandwidthMonitor(self.interface, 1)
self.host_watcher = HostWatcher(self.host_scanner, self._reconnect_callback)
self.host_watcher = HostWatcher(self.interface, self.iprange, self._reconnect_callback)

# holds discovered hosts
self.hosts = []
Expand Down Expand Up @@ -125,6 +127,16 @@ def _scan_handler(self, args):
else:
iprange = None

if args.intensity:
intensity = self._parse_scan_intensity(args.intensity)
if intensity is None:
IO.error('invalid intensity level.')
return
else:
intensity = ScanIntensity.NORMAL

self.host_scanner.set_intensity(intensity)

with self.hosts_lock:
for host in self.hosts:
self._free_host(host)
Expand Down Expand Up @@ -159,7 +171,7 @@ def _hosts_handler(self, args):
host.ip,
host.mac,
host.name,
host.pretty_status()
self.limiter.pretty_status(host)
])

table = SingleTable(table_data, 'Hosts')
Expand Down Expand Up @@ -269,7 +281,10 @@ def _monitor_handler(self, args):
"""
def get_bandwidth_results():
with self.hosts_lock:
return [x for x in [(y, self.bandwidth_monitor.get(y)) for y in self.hosts] if x[1] is not None]
return sorted(
[x for x in [(y, self.bandwidth_monitor.get(y)) for y in self.hosts] if x[1] is not None],
key=lambda h: not (h[0].limited or h[0].blocked)
)

def display(stdscr, interval):
host_results = get_bandwidth_results()
Expand All @@ -295,6 +310,7 @@ def display(stdscr, interval):

y_off += 2
x_off = x_rst
temps_reached = False

for host, result in host_results:
result_data = [
Expand All @@ -306,6 +322,10 @@ def display(stdscr, interval):
'{}↑ {}↓'.format(result.upload_total_count, result.download_total_count)
]

if not temps_reached and host in hosts_to_be_freed:
temps_reached = True
y_off += 1

for j, string in enumerate(result_data):
stdscr.addstr(y_off, x_off, string)
x_off += header_off[j][1]
Expand All @@ -323,6 +343,8 @@ def display(stdscr, interval):
except KeyboardInterrupt:
return

hosts = self._get_hosts_by_ids(args.id)
hosts_to_be_freed = set()

interval = 0.5 # in s
if args.interval:
Expand All @@ -332,6 +354,13 @@ def display(stdscr, interval):

interval = int(args.interval) / 1000 # from ms to s

for host in hosts:
if not host.spoofed:
hosts_to_be_freed.add(host)

self.arp_spoofer.add(host)
self.bandwidth_monitor.add(host)

if len(get_bandwidth_results()) == 0:
IO.error('no hosts to be monitored.')
return
Expand All @@ -341,6 +370,9 @@ def display(stdscr, interval):
except curses.error:
IO.error('monitor error occurred. maybe terminal too small?')

for host in hosts_to_be_freed:
self._free_host(host)

def _analyze_handler(self, args):
hosts = self._get_hosts_by_ids(args.id)
if hosts is None or len(hosts) == 0:
Expand Down Expand Up @@ -439,6 +471,7 @@ def _watch_handler(self, args):

iprange = self.host_watcher.iprange
interval = self.host_watcher.interval
intensity = self.host_watcher.intensity

set_table_data.append([
'{}range{}'.format(IO.Fore.LIGHTYELLOW_EX, IO.Style.RESET_ALL),
Expand All @@ -450,6 +483,11 @@ def _watch_handler(self, args):
'{}s'.format(interval)
])

set_table_data.append([
'{}intensity{}'.format(IO.Fore.LIGHTYELLOW_EX, IO.Style.RESET_ALL),
intensity
])

for host in self.host_watcher.hosts:
watch_table_data.append([
'{}{}{}'.format(IO.Fore.LIGHTYELLOW_EX, self._get_host_id(host), IO.Style.RESET_ALL),
Expand Down Expand Up @@ -517,6 +555,12 @@ def _watch_set_handler(self, args):
self.host_watcher.interval = int(args.value)
else:
IO.error('invalid interval.')
elif args.attribute.lower() in ('intensity', 'scan_intensity'):
intensity = self._parse_scan_intensity(args.value)
if intensity is not None:
self.host_watcher.intensity = intensity
else:
IO.error('invalid scan intensity level.')
else:
IO.error('{}{}{} is an invalid settings attribute.'.format(IO.Fore.LIGHTYELLOW_EX, args.attribute, IO.Style.RESET_ALL))

Expand Down Expand Up @@ -559,10 +603,10 @@ def _help_handler(self, args):
IO.print(
"""
{y}scan (--range [IP range]){r}{}scans for online hosts on your network.
{s}required to find the hosts you want to limit.
{y} (--intensity [(1,2,3)]){r}{}required to find the hosts you want to limit.
{b}{s}e.g.: scan
{s} scan --range 192.168.178.1-192.168.178.50
{s} scan --range 192.168.178.1/24{r}
{s} scan --range 192.168.178.1/24 --intensity 3{r}

{y}hosts (--force){r}{}lists all scanned hosts.
{s}contains host information, including IDs.
Expand All @@ -585,8 +629,8 @@ def _help_handler(self, args):
{b}{s}e.g.: add 192.168.178.24
{s} add 192.168.1.50 --mac 1c:fc:bc:2d:a6:37{r}

{y}monitor (--interval [time in ms]){r}{}monitors bandwidth usage of limited host(s).
{b}{s}e.g.: monitor --interval 600{r}
{y}monitor [ID1,ID2,...]{r}{}monitors bandwidth usage of host(s).
{y} (--interval [time in ms]){r}{}{b}e.g.: monitor all --interval 600{r}

{y}analyze [ID1,ID2,...]{r}{}analyzes traffic of host(s) without limiting
{y} (--duration [time in s]){r}{}to determine who uses how much bandwidth.
Expand All @@ -598,21 +642,24 @@ def _help_handler(self, args):
{y}watch remove [ID1,ID2,...]{r}{}removes host from the reconnection watchlist.
{b}{s}e.g.: watch remove all{r}
{y}watch set [attr] [value]{r}{}changes reconnect watch settings.
{b}{s}e.g.: watch set interval 120{r}
{b}{s}e.g.: watch set interval 120
{s} watch set intensity 1{r}

{y}clear{r}{}clears the terminal window.

{y}quit{r}{}quits the application.
""".format(
spaces[len('scan (--range [IP range])'):],
spaces[len(' (--intensity [(1,2,3)])'):],
spaces[len('hosts (--force)'):],
spaces[len('limit [ID1,ID2,...] [rate]'):],
spaces[len(' (--upload) (--download)'):],
spaces[len('block [ID1,ID2,...]'):],
spaces[len(' (--upload) (--download)'):],
spaces[len('free [ID1,ID2,...]'):],
spaces[len('add [IP] (--mac [MAC])'):],
spaces[len('monitor (--interval [time in ms])'):],
spaces[len('monitor [ID1,ID2,...]'):],
spaces[len(' (--interval [time in ms])'):],
spaces[len('analyze [ID1,ID2,...]'):],
spaces[len(' (--duration [time in s])'):],
spaces[len('watch'):],
Expand Down Expand Up @@ -705,6 +752,10 @@ def _parse_iprange(self, range):
except netaddr.core.AddrFormatError:
return

def _parse_scan_intensity(self, value):
if value.isdigit() and int(value) in (ScanIntensity.QUICK, ScanIntensity.NORMAL, ScanIntensity.INTENSE):
return int(value)

def _free_host(self, host):
"""
Stops ARP spoofing and unlimits host
Expand Down
10 changes: 1 addition & 9 deletions evillimiter/networking/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,4 @@ def __eq__(self, other):
return self.ip == other.ip

def __hash__(self):
return hash((self.mac, self.ip))

def pretty_status(self):
if self.limited:
return '{}Limited{}'.format(IO.Fore.LIGHTRED_EX, IO.Style.RESET_ALL)
elif self.blocked:
return '{}Blocked{}'.format(IO.Fore.RED, IO.Style.RESET_ALL)
else:
return 'Free'
return hash((self.mac, self.ip))
28 changes: 28 additions & 0 deletions evillimiter/networking/limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import evillimiter.console.shell as shell
from .host import Host
from evillimiter.common.globals import BIN_TC, BIN_IPTABLES
from evillimiter.console.io import IO


class Limiter(object):
Expand Down Expand Up @@ -90,6 +91,33 @@ def replace(self, old_host, new_host):
else:
self.limit(new_host, info['direction'], info['rate'])

def pretty_status(self, host):
"""
Gets the host limitation status in a formatted and colored string
"""
with self._host_dict_lock:
if host in self._host_dict:
rate = self._host_dict[host]['rate']
direction = self._host_dict[host]['direction']
uload = None
dload = None
final = ''

if direction in (Direction.BOTH, Direction.OUTGOING):
uload = '0bit' if rate is None else rate
if direction in (Direction.BOTH, Direction.INCOMING):
dload = '0bit' if rate is None else rate

if uload is not None:
final += '{}↑'.format(uload)
if dload is not None:
final += ' {}↓'.format(dload)

return '{}{}{}'.format(IO.Fore.LIGHTYELLOW_EX, final.strip(), IO.Style.RESET_ALL)

else:
return '{}Free{}'.format(IO.Fore.LIGHTGREEN_EX, IO.Style.RESET_ALL)

def _new_host_limit_ids(self, host, direction):
"""
Get limit information for corresponding host
Expand Down
49 changes: 40 additions & 9 deletions evillimiter/networking/scan.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
import socket
import threading
import collections
from tqdm import tqdm
from netaddr import IPAddress
from scapy.all import sr1, ARP # pylint: disable=no-name-in-module
Expand All @@ -10,18 +12,39 @@


class HostScanner(object):
Settings = collections.namedtuple('Settings', 'max_workers retries timeout')

def __init__(self, interface, iprange):
self.interface = interface
self.iprange = iprange

self.max_workers = 75 # max. amount of threads
self.retries = 0 # ARP retry
self.timeout = 2.5 # time in s to wait for an answer
self._quick_settings = HostScanner.Settings(max_workers=80, retries=0, timeout=2)
self._normal_settings = HostScanner.Settings(max_workers=80, retries=1, timeout=3)
self._intense_settings = HostScanner.Settings(max_workers=80, retries=5, timeout=10)

def scan(self, iprange=None):
self._resolve_names = True
self._settings = self._normal_settings
self._settings_lock = threading.Lock()

@property
def settings(self):
with self._settings_lock:
return self._settings

@settings.setter
def settings(self, value):
with self._settings_lock:
self._settings = value

def set_intensity(self, intensity):
if intensity == ScanIntensity.QUICK:
self.settings = self._quick_settings
elif intensity == ScanIntensity.NORMAL:
self.settings = self._normal_settings
elif intensity == ScanIntensity.INTENSE:
self.settings = self._intense_settings

with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
def scan(self, iprange=None):
with ThreadPoolExecutor(max_workers=self.settings.max_workers) as executor:
hosts = []
iprange = [str(x) for x in (self.iprange if iprange is None else iprange)]
iterator = tqdm(
Expand Down Expand Up @@ -49,7 +72,7 @@ def scan(self, iprange=None):
return hosts

def scan_for_reconnects(self, hosts, iprange=None):
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
with ThreadPoolExecutor(max_workers=self.settings.max_workers) as executor:
scanned_hosts = []
iprange = [str(x) for x in (self.iprange if iprange is None else iprange)]
for host in executor.map(self._sweep, iprange):
Expand All @@ -70,8 +93,16 @@ def _sweep(self, ip):
Sends ARP packet and listens for answer,
if present the host is online
"""
settings = self.settings

packet = ARP(op=1, pdst=ip)
answer = sr1(packet, retry=self.retries, timeout=self.timeout, verbose=0, iface=self.interface)
answer = sr1(packet, retry=settings.retries, timeout=settings.timeout, verbose=0, iface=self.interface)

if answer is not None:
return Host(ip, answer.hwsrc, '')
return Host(ip, answer.hwsrc, '')


class ScanIntensity:
QUICK = 1
NORMAL = 2
INTENSE = 3
Loading