├── main.py ├── .gitignore ├── requirements.txt ├── requirements3.txt ├── microlimit.sh ├── readme.md ├── process_finder.py ├── scapy_watcher.py ├── tc.sh ├── nethogs.py ├── packet_watcher.py ├── packet_limiter.py └── kivy_ui.py /main.py: -------------------------------------------------------------------------------- 1 | """main.py: Dummo file for convenience of launching.""" 2 | 3 | __author__ = "Alex 'Chozabu' P-B" 4 | __copyright__ = "Copyright 2016, IAgree" 5 | 6 | import kivy_ui 7 | 8 | kivy_ui.mainapp.run() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | atestlimit.sh 4 | env/ 5 | env3/ 6 | limit_ports.pyc 7 | nenv/ 8 | ppt.py 9 | ptestlimit.sh 10 | qtgui.py 11 | simple_packet_print.pyc 12 | test_gui.py 13 | testlimit.sh 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chains==0.1.17 2 | dpkt==1.8.8 3 | Kivy==1.9.1 4 | Kivy-Garden==0.1.4 5 | netifaces==0.10.5 6 | Pillow==3.4.2 7 | pygame==1.9.2b8 8 | pypcap==1.1.4.1 9 | requests==2.12.3 10 | scapy==2.3.3 11 | -------------------------------------------------------------------------------- /requirements3.txt: -------------------------------------------------------------------------------- 1 | Kivy==1.9.1 2 | Kivy-Garden==0.1.4 3 | Logbook==1.0.0 4 | lxml==3.6.4 5 | Pillow==3.4.2 6 | py==1.4.31 7 | pygame==1.9.2b8 8 | pyshark==0.3.6.1 9 | requests==2.12.3 10 | scapy-python3==0.20 11 | trollius==1.0.4 12 | -------------------------------------------------------------------------------- /microlimit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o xtrace 4 | 5 | IF="wlp3s0" 6 | LIMIT="10000kbit" 7 | BURST="100kbit" 8 | PORT="80" 9 | 10 | echo "resetting tc" 11 | tc qdisc del dev ${IF} root 12 | tc qdisc del dev ${IF} ingress 13 | 14 | echo "setting tc" 15 | tc qdisc add dev ${IF} handle ffff: ingress 16 | #tc filter add dev ${IF} parent ffff: protocol ip prio 1 \ 17 | # u32 match ip dport ${PORT} 0xffff police rate ${LIMIT} \ 18 | # burst $BURST flowid :1 19 | #tc filter add dev ${IF} parent ffff: protocol ip prio 1 \ 20 | # u32 match ip sport ${PORT} 0xffff police rate ${LIMIT} \ 21 | # burst $BURST flowid :1 22 | 23 | tc filter add dev ${IF} parent ffff: \ 24 | protocol ip prio 1 \ 25 | u32 match ip dport ${PORT} 0xffff \ 26 | police rate ${LIMIT} burst $BURST drop \ 27 | flowid :1 28 | tc filter add dev ${IF} parent ffff: \ 29 | protocol ip prio 1 \ 30 | u32 match ip sport ${PORT} 0xffff \ 31 | police rate ${LIMIT} burst $BURST drop \ 32 | flowid :1 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #LinNetLim 2 | 3 | This is a program to show how which TCP ports are using data, and limit it - similar to "NetLimiter" for Windows 4 | 5 | Currently downstream limiting is disabled - if you have the time and skill to fix this, please send a PR! 6 | 7 | GUI is in Kivy 8 | chains is used to read current net usage 9 | bash, tc and iptables are used to impose port limits 10 | 11 | Do not use if you have a custom iptables setup! This will wipe any custom rules (from the mangle table, and probably others too) 12 | 13 | ![screenshot here](http://chozabu.net/autopush/2016-12-03-00-07-094395124.png) 14 | 15 | ##Running 16 | 17 | first intall libpcap-dev 18 | 19 | sudo apt install libpcap-dev 20 | 21 | Create a python env, install requirements (using requirements.txt for py2 and requirements3.txt for py3) then run the UI as root 22 | 23 | virtualenv env 24 | . env/bin/activate 25 | pip install -r requirements.txt 26 | sudo env/bin/python kivy_ui.py 27 | 28 | toggle the button next to ports you want to limit, type in an up/down limit in kb and press apply 29 | 30 | To clear limits - uncheck all limits and press apply - closing the program will not stop limiting 31 | 32 | ##TODO 33 | 34 | - group by process (perhaps with lsof?) 35 | - separate up/down data measurements 36 | - tidy up 37 | - system tray icon 38 | - curses CLI (like iftop, nethogs) 39 | -------------------------------------------------------------------------------- /process_finder.py: -------------------------------------------------------------------------------- 1 | """process_finder.py: has a dict of PIDs with the ports they are listening on, and a lookup dict of port->pid""" 2 | 3 | __author__ = "Alex 'Chozabu' P-B" 4 | __copyright__ = "Copyright 2016, Chozabu" 5 | 6 | 7 | import subprocess 8 | 9 | process_info = {} 10 | port_lookup = {} 11 | 12 | def run(commands): 13 | process = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE) 14 | return process.communicate(commands.encode()) 15 | 16 | 17 | def refresh_port_info(): 18 | out, err = run("lsof -i -F cpn") 19 | out = out.decode() 20 | out = out.split("\n") 21 | currentUID = -1 22 | currentCMD = "unknown" 23 | currentPID = -1 24 | for line in out: 25 | if not line: continue 26 | if line[0]=='c': 27 | currentCMD=line[1:] 28 | if line[0]=='p': 29 | currentPID=int(line[1:]) 30 | if line[0]=='f': 31 | currentUID=int(line[1:]) 32 | if line[0]=='n': 33 | port = line.split(":")[-1] 34 | if port == "http": 35 | port=80 36 | elif port == "https": 37 | port = 443 38 | else: 39 | try: 40 | port=int(port) 41 | except: 42 | pass 43 | process_info[currentPID] = { 44 | "PID": currentPID, 45 | "UID": currentUID, 46 | "CMD": currentCMD, 47 | "NAME": line[1:], 48 | "PORT": port 49 | } 50 | port_lookup[port] = currentPID 51 | 52 | 53 | #print("done\n", out) 54 | 55 | if __name__ == '__main__': 56 | refresh_port_info() 57 | #print(process_info) 58 | 59 | -------------------------------------------------------------------------------- /scapy_watcher.py: -------------------------------------------------------------------------------- 1 | """scapy_watcher.py: watches network packets, calculates speed and totals per-port.""" 2 | 3 | __author__ = "Alex 'Chozabu' P-B" 4 | __copyright__ = "Copyright 2016, Chozabu" 5 | 6 | 7 | import netifaces 8 | 9 | from threading import Thread 10 | from scapy.all import * 11 | 12 | last_time = time.time() 13 | 14 | portcounts = {} 15 | 16 | def calc_speeds(): 17 | global last_time 18 | # update speed counters 19 | new_time = time.time() 20 | if new_time > last_time+1: 21 | last_time+=1 22 | for k in portcounts.keys(): 23 | v = portcounts[k] 24 | diff = v['total'] - v['last'] 25 | v['last'] = v['total'] 26 | v['speed_raw'] = diff 27 | v['speed'] = v['speed']*.9 + diff *.1 28 | 29 | #find and print port using most bandwidth each second 30 | topport = -1 31 | topport_speed = -1 32 | for k in portcounts.keys(): 33 | v = portcounts[k] 34 | if v['speed'] > topport_speed: 35 | topport = k 36 | topport_speed = v['speed'] 37 | if topport != -1: 38 | print("heaviest port:", topport, portcounts[topport]) 39 | 40 | def pkt_callback(pkt): 41 | #pkt.show() # debug statement 42 | #print(pkt.fields) 43 | #print(pkt.fields_desc) 44 | #print("dport", pkt['TCP'].dport, "sport", pkt['TCP'].sport) 45 | #print() 46 | 47 | datalen = len(pkt) 48 | sport = pkt['TCP'].sport 49 | 50 | if sport not in portcounts: 51 | portcounts[sport] = {"total": 0, "last": 0, "speed_raw": 0, "speed": 0, "port": sport} 52 | portcounts[sport]['total'] += datalen 53 | 54 | calc_speeds() 55 | 56 | def run(): 57 | ifaces = [iface for iface in netifaces.interfaces() 58 | if netifaces.AF_INET in netifaces.ifaddresses(iface)] 59 | sniff(iface=ifaces, prn=pkt_callback, filter="tcp", store=0) 60 | 61 | 62 | def launch_watcher(): 63 | run() 64 | 65 | def start_background_thread(): 66 | try: 67 | t = Thread( 68 | daemon=True, 69 | target=launch_watcher, 70 | kwargs={}) 71 | except: 72 | t = Thread( 73 | target=launch_watcher, 74 | kwargs={}) 75 | t.daemon = True 76 | t.start() 77 | 78 | if __name__ == '__main__': 79 | run() 80 | -------------------------------------------------------------------------------- /tc.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 The Bitcoin Core developers & Chozabu 2 | # Distributed under the MIT software license, see the accompanying 3 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 | 5 | #this altered version of tc.sh not used by LinNetLim - but kepts as a reference 6 | 7 | #network interface on which to limit traffic 8 | IF="wlp3s0" 9 | #limit of the network interface in question 10 | LINKCEIL="1gbit" 11 | #limit outbound in/out on selected port to this rate 12 | LIMIT="1000kbit" 13 | BURST="15k" 14 | PORT="80" 15 | #defines the address space for which you wish to disable rate limiting 16 | LOCALNET="192.168.0.0/16" 17 | 18 | 19 | 20 | #delete existing rules 21 | echo "resetting tc" 22 | tc qdisc del dev ${IF} root 23 | tc qdisc del dev ${IF} ingress 24 | 25 | #delete any existing rules 26 | echo "resetting iptables" 27 | ipt="/sbin/iptables" 28 | ## Failsafe - die if /sbin/iptables not found 29 | [ ! -x "$ipt" ] && { echo "$0: \"${ipt}\" command not found."; exit 1; } 30 | $ipt -P INPUT ACCEPT 31 | $ipt -P FORWARD ACCEPT 32 | $ipt -P OUTPUT ACCEPT 33 | $ipt -F INPUT -t mangle 34 | $ipt -F OUTPUT -t mangle 35 | $ipt -F FORWARD -t mangle 36 | $ipt -t mangle -F 37 | $ipt -t mangle -X 38 | 39 | 40 | echo "setting tc" 41 | 42 | #add root class 43 | tc qdisc add dev ${IF} root handle 1: htb default 10 44 | tc qdisc add dev ${IF} handle ffff: ingress 45 | tc filter add dev ${IF} parent ffff: protocol ip prio 1 \ 46 | u32 match ip dport ${PORT} 0xffff police rate ${LIMIT} \ 47 | burst $BURST flowid :1 48 | tc filter add dev ${IF} parent ffff: protocol ip prio 1 \ 49 | u32 match ip sport ${PORT} 0xffff police rate ${LIMIT} \ 50 | burst $BURST flowid :1 51 | 52 | tc filter add dev wlp3s0 parent ffff: protocol ip prio 1 \ 53 | u32 match ip dport 80 0xffff police rate 5kbit \ 54 | burst $BURST flowid :1 55 | 56 | 57 | 58 | #tc filter add dev ${IF} parent ffff: protocol ip prio 50 u32 match ip src \ 59 | # 0.0.0.0/0 police rate ${LIMIT} burst 20k drop flowid :1 60 | 61 | #add parent class 62 | tc class add dev ${IF} parent 1: classid 1:1 htb rate ${LINKCEIL} ceil ${LINKCEIL} 63 | 64 | #add our two classes. one unlimited, another limited 65 | tc class add dev ${IF} parent 1:1 classid 1:10 htb rate ${LINKCEIL} ceil ${LINKCEIL} prio 0 66 | 67 | 68 | 69 | ##loop this for each rule 70 | #tc class add dev ${IF} parent 1:1 classid 1:11 htb rate ${LIMIT} ceil ${LIMIT} prio 1 71 | 72 | #tc class add dev ${IF} parent 1:1 classid 1:13 htb rate ${LIMIT} burst 15k 73 | tc class add dev ${IF} parent 1:1 classid 1:14 htb rate ${LIMIT} burst 15k 74 | 75 | #add handles to our classes so packets marked with go into the class with "... handle fw ..." 76 | #tc filter add dev ${IF} parent 1: protocol ip prio 1 handle 1 fw classid 1:10 77 | 78 | 79 | 80 | ##loop this for each rule 81 | #tc filter add dev ${IF} parent 1: protocol ip prio 2 handle 6 fw classid 1:13 82 | #tc filter add dev ${IF} parent 1: protocol ip prio 2 handle 7 fw classid 1:13 83 | tc filter add dev ${IF} parent 1: protocol ip prio 2 handle 8 fw classid 1:14 84 | 85 | 86 | ##loop this for each rule 87 | echo "setting iptables" 88 | #limit outgoing traffic to and from port 8333. but not when dealing with a host on the local network 89 | # (defined by $LOCALNET) 90 | # --set-mark marks packages matching these criteria with the number "2" 91 | # these packages are filtered by the tc filter with "handle 2" 92 | # this filter sends the packages into the 1:11 class, and this class is limited to ${LIMIT} 93 | iptables -t mangle -A OUTPUT -p tcp -m tcp --dport ${PORT} ! -d ${LOCALNET} -j MARK --set-mark 8 94 | iptables -t mangle -A OUTPUT -p tcp -m tcp --sport ${PORT} ! -d ${LOCALNET} -j MARK --set-mark 8 95 | #limit incoming traffic 96 | #iptables -t mangle -A INPUT -p tcp -m tcp --dport ${PORT} -j MARK --set-mark 7 97 | #iptables -t mangle -A INPUT -p tcp -m tcp --sport ${PORT} -j MARK --set-mark 7 98 | 99 | 100 | -------------------------------------------------------------------------------- /nethogs.py: -------------------------------------------------------------------------------- 1 | """nethogs.py: from https://github.com/akshayKMR/hogwatch/blob/master/hogwatch/server/watchdogs/nethogs.py.""" 2 | 3 | import subprocess, sys 4 | try: 5 | from queue import Queue 6 | except: 7 | from Queue import Queue#py2 8 | from pprint import pprint 9 | from decimal import Decimal 10 | import time 11 | 12 | import atexit 13 | 14 | 15 | class NethogsWatchdog: 16 | def __init__(self, debug=False, devices=[], delay=1): 17 | 18 | from sys import platform as _platform 19 | if _platform == "linux" or _platform == "linux2": 20 | pass 21 | # linux 22 | elif _platform == "darwin": 23 | if len(devices) == 0: 24 | # devices=['en0'] 25 | pass 26 | elif _platform == "win32": 27 | print 28 | "Windows is not supported." 29 | self.devices = devices 30 | self.delay = str(delay) 31 | self.debug = debug 32 | 33 | self._running = True 34 | 35 | def terminate(self): 36 | self._running = False 37 | 38 | def watch_transfer(self, mode='transfer_rate', bridge={}): 39 | # param 0=rate, 3 amount in MB 40 | 41 | if mode not in ['transfer_rate', 'transfer_amount']: 42 | raise ValueError('mode not supported') 43 | 44 | if mode == 'transfer_rate': 45 | param = '0' 46 | else: 47 | param = '1' 48 | 49 | cmd = ['nethogs', '-d', self.delay, '-v', param, '-t'] + self.devices 50 | if self.debug: 51 | print 52 | cmd 53 | 54 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1) 55 | atexit.register(p.terminate) 56 | 57 | # need json output :/ 58 | refresh_flag = True 59 | report = {} 60 | report['mode'] = mode 61 | report['running'] = True 62 | report['ctr'] = 0 63 | 64 | entries = [] 65 | for line in iter(p.stdout.readline, b''): 66 | 67 | if (self._running == False): 68 | break 69 | print(line) 70 | 71 | # print line 72 | if (line.find(b'Refreshing') == -1): 73 | 74 | if refresh_flag: 75 | continue 76 | split = line.split() 77 | if (len(split) != 3): 78 | continue 79 | 80 | entry = {} 81 | pline = split[0].decode() 82 | psplit = pline.split('/') 83 | entry['process'] = pline 84 | entry['path'] = "/".join(psplit[:-2]) 85 | entry['pid'] = psplit[-2] 86 | entry['filename'] = psplit[-3] 87 | 88 | if (mode == 'transfer_rate'): 89 | entry['kbps_out'] = float(split[1]) 90 | entry['kbps_in'] = float(split[2]) 91 | else: # mode is 'transfer_amount' 92 | entry['kb_out'] = float(split[1]) 93 | entry['kb_in'] = float(split[2]) 94 | 95 | entries.append(entry) 96 | else: 97 | if refresh_flag: 98 | refresh_flag = False 99 | continue 100 | 101 | if (len(entries) == 0): 102 | continue 103 | 104 | total_in, total_out = 0, 0 105 | for entry in entries: 106 | if (report['mode'] == 'transfer_rate'): 107 | total_in += entry['kbps_in'] 108 | total_out += entry['kbps_out'] 109 | else: 110 | total_in += entry['kb_in'] 111 | total_out += entry['kb_out'] 112 | 113 | report['total_in'] = total_in 114 | report['total_out'] = total_out 115 | report['entries'] = entries 116 | report['ctr'] += 1 117 | entries = [] 118 | 119 | if self.debug: 120 | print(line) 121 | pprint(report) 122 | else: 123 | report['timestamp'] = int(round(time.time() * 1000)) # js time format 124 | bridge['queue'].put(report) 125 | 126 | p.terminate() 127 | if self.debug: 128 | print 129 | 'exited nethogs' 130 | else: 131 | report = {} 132 | report['running'] = False 133 | bridge['queue'].put(report) 134 | 135 | 136 | if __name__ == '__main__': 137 | x = NethogsWatchdog(debug=True, devices=sys.argv[1:]) 138 | x.watch_transfer() -------------------------------------------------------------------------------- /packet_watcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Example: Simple Packet Printer """ 3 | import os 4 | import argparse 5 | 6 | from threading import Thread 7 | 8 | # Local imports 9 | from chains.utils import signal_utils 10 | from chains.sources import packet_streamer 11 | from chains.links import packet_meta, reverse_dns, transport_meta 12 | from chains.sinks import packet_printer, packet_summary 13 | 14 | import time 15 | 16 | from chains.sinks import sink 17 | from chains.utils import net_utils 18 | 19 | portcounts = {} 20 | 21 | 22 | class PacketCounter(sink.Sink): 23 | """Print packet information""" 24 | 25 | def __init__(self, color_output=True): 26 | """Initialize PacketPrinter Class""" 27 | 28 | # Call super class init 29 | super(PacketCounter, self).__init__() 30 | 31 | # Should we add color on the output 32 | self._color = color_output 33 | self.last_time = time.time() 34 | 35 | def pull(self): 36 | """Print out information about each packet from the input_stream""" 37 | 38 | # For each packet in the pcap process the contents 39 | for item in self.input_stream: 40 | 41 | # Unpack the Ethernet frame (mac src/dst, ethertype) 42 | #print 'Ethernet Frame: %s --> %s (type: %d)' % \ 43 | # (net_utils.mac_to_str(item['eth']['src']), net_utils.mac_to_str(item['eth']['dst']), item['eth']['type']) 44 | 45 | # gather data usage per port 46 | if item['transport']: 47 | transport_info = item['transport'] 48 | data = transport_info['data'] 49 | datalen = len(data) 50 | 51 | if 'sport' in transport_info and datalen: 52 | sport = transport_info['sport'] 53 | if sport not in portcounts: 54 | portcounts[sport] = {"total": 0,"last": 0,"speed_raw": 0,"speed": 0, "port":sport} 55 | portcounts[sport]['total'] += datalen 56 | #print(portcounts) 57 | 58 | # update speed counters 59 | new_time = time.time() 60 | if new_time > self.last_time+1: 61 | self.last_time+=1 62 | for k,v in portcounts.iteritems(): 63 | diff = v['total'] - v['last'] 64 | v['last'] = v['total'] 65 | v['speed_raw'] = diff 66 | v['speed'] = v['speed']*.9 + diff *.1 67 | 68 | #find and print port using most bandwidth each second 69 | topport = -1 70 | topport_speed = -1 71 | for k,v in portcounts.iteritems(): 72 | if v['speed'] > topport_speed: 73 | topport = k 74 | topport_speed = v['speed'] 75 | if topport != -1: 76 | print("heaviest port:", topport, portcounts[topport]) 77 | 78 | def run(iface_name=None, bpf=None, summary=None, max_packets=100): 79 | """Run the Simple Packet Printer Example""" 80 | 81 | # Create the classes 82 | streamer = packet_streamer.PacketStreamer(iface_name=iface_name, bpf=bpf, max_packets=max_packets) 83 | meta = packet_meta.PacketMeta() 84 | rdns = reverse_dns.ReverseDNS() 85 | tmeta = transport_meta.TransportMeta() 86 | printer = PacketCounter() 87 | 88 | # Set up the chain 89 | meta.link(streamer) 90 | rdns.link(meta) 91 | tmeta.link(rdns) 92 | printer.link(tmeta) 93 | 94 | # Pull the chain 95 | printer.pull() 96 | 97 | def test(): 98 | """Test the Simple Packet Printer Example""" 99 | from chains.utils import file_utils 100 | 101 | # For the test we grab a file, but if you don't specify a 102 | # it will grab from the first active interface 103 | data_path = file_utils.relative_dir(__file__, '../data/http.pcap') 104 | run(iface_name = data_path) 105 | 106 | def my_exit(): 107 | """Exit on Signal""" 108 | print 'Goodbye...' 109 | 110 | 111 | def launch_watcher(): 112 | run(max_packets=None) 113 | 114 | def start_background_thread(): 115 | t = Thread( 116 | target=launch_watcher, 117 | kwargs={}) 118 | t.daemon = True 119 | t.start() 120 | 121 | if __name__ == '__main__': 122 | 123 | # Collect args from the command line 124 | parser = argparse.ArgumentParser() 125 | parser.add_argument('-bpf', type=str, help='BPF Filter for PacketStream Class') 126 | parser.add_argument('-s','--summary', action="store_true", help='Summary instead of full packet print') 127 | parser.add_argument('-m','--max-packets', type=int, default=10, help='How many packets to process (0 for infinity)') 128 | parser.add_argument('-p','--pcap', type=str, help='Specify a pcap file instead of reading from live network interface') 129 | args, commands = parser.parse_known_args() 130 | if commands: 131 | print 'Unrecognized args: %s' % commands 132 | try: 133 | # Pcap file may have a tilde in it 134 | if args.pcap: 135 | args.pcap = os.path.expanduser(args.pcap) 136 | 137 | with signal_utils.signal_catcher(my_exit): 138 | run(iface_name=args.pcap, bpf=args.bpf, summary=args.summary, max_packets=args.max_packets) 139 | except KeyboardInterrupt: 140 | print 'Goodbye...' 141 | -------------------------------------------------------------------------------- /packet_limiter.py: -------------------------------------------------------------------------------- 1 | """Foobar.py: Description of what foobar does.""" 2 | 3 | __author__ = "Alex 'Chozabu' P-B" 4 | __copyright__ = "Copyright 2016, IAgree" 5 | 6 | import subprocess 7 | 8 | interface = "wlp3s0" 9 | LINKCEIL="1gbit" 10 | ipt="/sbin/iptables" 11 | LOCALNET="192.168.0.0/16" 12 | 13 | traffic_classes = { 14 | 11: { 15 | "mark":33, 16 | "limit":50 17 | }, 18 | 12: { 19 | "mark":44, 20 | "limit":90 21 | } 22 | } 23 | 24 | port_limits = { 25 | 80: { 26 | "up":33, 27 | "down":33 28 | }, 29 | 48180: { 30 | "up":33, 31 | "down":33 32 | }, 33 | 49144: { 34 | "up":33, 35 | "down":33 36 | }, 37 | 57084: { 38 | "up":33, 39 | "down":33 40 | }, 41 | 5001: { 42 | "up":33, 43 | "down":44 44 | } 45 | } 46 | 47 | 48 | 49 | reset_net = ''' 50 | # Copyright (c) 2013 The Bitcoin Core developers & Chozabu 51 | # Distributed under the MIT software license, see the accompanying 52 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 53 | 54 | #limit of the network interface in question 55 | LINKCEIL="1gbit" 56 | 57 | #defines the address space for which you wish to disable rate limiting 58 | 59 | 60 | #delete existing rules 61 | echo "resetting tc" 62 | tc qdisc del dev {interface} root 63 | 64 | #delete any existing rules 65 | echo "resetting iptables" 66 | ipt="{ipt}" 67 | 68 | $ipt -P INPUT ACCEPT 69 | $ipt -P FORWARD ACCEPT 70 | $ipt -P OUTPUT ACCEPT 71 | $ipt -F INPUT -t mangle 72 | $ipt -F OUTPUT -t mangle 73 | $ipt -F FORWARD -t mangle 74 | $ipt -t mangle -F 75 | $ipt -t mangle -X 76 | '''.format(interface=interface, ipt=ipt) 77 | 78 | 79 | 80 | basic_net = ''' 81 | echo "setting tc" 82 | 83 | #add root class 84 | tc qdisc add dev {interface} root handle 1: htb default 10 85 | 86 | tc qdisc add dev {interface} handle ffff: ingress 87 | 88 | #add parent class 89 | tc class add dev {interface} parent 1: classid 1:1 htb rate {LINKCEIL} ceil {LINKCEIL} 90 | 91 | #add our two classes. one unlimited, another limited 92 | tc class add dev {interface} parent 1:1 classid 1:10 htb rate {LINKCEIL} ceil {LINKCEIL} prio 0 93 | 94 | #add handles to our classes so packets marked with go into the class with "... handle fw ..." 95 | tc filter add dev {interface} parent 1: protocol ip prio 1 handle 1 fw classid 1:10 96 | '''.format(interface=interface, LINKCEIL=LINKCEIL) 97 | 98 | 99 | tclass = "tc class add dev {interface} parent 1:1 classid 1:{tcid} htb rate {LIMIT}kbit ceil {LIMIT}kbit prio 1" 100 | tfilter = "tc filter add dev {interface} parent 1: protocol ip prio 2 handle {mark} fw classid 1:{tcid}" 101 | 102 | 103 | markout = ''' 104 | iptables -t mangle -A OUTPUT -p tcp -m tcp --dport {PORT} ! -d {LOCALNET} -j MARK --set-mark {mark} 105 | iptables -t mangle -A OUTPUT -p tcp -m tcp --sport {PORT} ! -d {LOCALNET} -j MARK --set-mark {mark} 106 | ''' 107 | 108 | markin = ''' 109 | tc filter add dev {interface} parent ffff: protocol ip prio 1 \ 110 | u32 match ip dport {PORT} 0xffff police rate {LIMIT}kbit \ 111 | burst 15k flowid :1 112 | tc filter add dev {interface} parent ffff: protocol ip prio 1 \ 113 | u32 match ip sport {PORT} 0xffff police rate {LIMIT}kbit \ 114 | burst 15k flowid :1 115 | ''' 116 | 117 | def run(commands): 118 | process = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE) 119 | return process.communicate(commands.encode()) 120 | 121 | def reset_all(): 122 | out, err = run(reset_net) 123 | print("done\n", out) 124 | 125 | 126 | def set_basics(): 127 | out, err = run(basic_net) 128 | print("done\n", out) 129 | 130 | def set_limits(): 131 | for k, v in traffic_classes.items(): 132 | ccmd = tclass.format(interface=interface, tcid=k, LIMIT=v['limit']) 133 | out, err = run(ccmd) 134 | print("set class for",k,v, out, ccmd) 135 | tcmd = tfilter.format(interface=interface, tcid=k, mark=v['mark']) 136 | out, err = run(tcmd) 137 | print("set filter for",k,v, out, tcmd) 138 | 139 | for k, v in port_limits.items(): 140 | if 'up' in v: 141 | ocmd = markout.format(LOCALNET=LOCALNET, PORT=k, mark=v['up']) 142 | out, err = run(ocmd) 143 | print("set uplimit for",k,v, out, ocmd) 144 | #disable downstream limiting until fixed 145 | '''if 'down' in v: 146 | print(v) 147 | icmd = markin.format(interface=interface, PORT=k, 148 | LIMIT=traffic_classes[v['down']]['limit']) 149 | print(icmd) 150 | out, err = run(icmd) 151 | print("set downlimit for",k,v, out, icmd)''' 152 | 153 | def set_from_ports_list(port_dict): 154 | global traffic_classes, port_limits 155 | traffic_classes = {} 156 | port_limits = {} 157 | 158 | class_lookup = {} 159 | 160 | currentClass = 11 161 | for d in port_dict: 162 | ul = d['up_limit'] 163 | dl = d['down_limit'] 164 | prt = d['port'] 165 | new_info = {} 166 | 167 | if ul not in class_lookup: 168 | traffic_classes[currentClass] = { 169 | 'mark': currentClass, 170 | 'limit': ul 171 | } 172 | class_lookup[ul] = currentClass 173 | new_info['up'] = currentClass 174 | currentClass += 1 175 | else: 176 | new_info['up'] = class_lookup[ul] 177 | 178 | if dl not in class_lookup: 179 | traffic_classes[currentClass] = { 180 | 'mark': currentClass, 181 | 'limit': dl 182 | } 183 | class_lookup[dl] = currentClass 184 | new_info['down'] = currentClass 185 | currentClass += 1 186 | else: 187 | new_info['down'] = class_lookup[dl] 188 | 189 | port_limits[prt] = new_info 190 | 191 | print("PORT LIMITS", port_limits) 192 | print("TClasses", traffic_classes) 193 | reset_all() 194 | set_basics() 195 | set_limits() 196 | 197 | if __name__ == '__main__': 198 | reset_all() 199 | set_basics() 200 | set_limits() 201 | -------------------------------------------------------------------------------- /kivy_ui.py: -------------------------------------------------------------------------------- 1 | 2 | from kivy.app import App 3 | from kivy.uix.widget import Widget 4 | from kivy.uix.button import Button 5 | from kivy.uix.togglebutton import ToggleButton 6 | from kivy.uix.label import Label 7 | from kivy.uix.boxlayout import BoxLayout 8 | from kivy.uix.gridlayout import GridLayout 9 | from kivy.uix.textinput import TextInput 10 | from kivy.uix.scrollview import ScrollView 11 | from kivy.clock import Clock 12 | from kivy.graphics import Color, Rectangle 13 | 14 | #import packet_watcher 15 | import packet_limiter 16 | import scapy_watcher as packet_watcher 17 | 18 | from kivy.config import Config 19 | 20 | Config.set('graphics', 'width', '1000') 21 | Config.set('graphics', 'height', '1000') 22 | 23 | 24 | class PortInfo(BoxLayout): 25 | def __init__(self,port, item, **kwargs): 26 | super(PortInfo, self).__init__(**kwargs) 27 | i = item#kwargs['item'] 28 | self.net_data = i 29 | 30 | #port = kwargs['port'] 31 | 32 | self.port_label = Label(text=str(port)) 33 | self.total_label = Label() 34 | self.raw_speed_label = Label() 35 | self.speed_label = Label() 36 | self.add_widget(self.port_label) 37 | self.add_widget(self.total_label) 38 | self.add_widget(self.raw_speed_label) 39 | self.add_widget(self.speed_label) 40 | 41 | self.up_limit = TextInput() 42 | self.down_limit = TextInput() 43 | self.down_limit.disabled = True 44 | self.enable_limit = ToggleButton(text="limit?") 45 | self.add_widget(self.up_limit) 46 | self.add_widget(self.down_limit) 47 | self.add_widget(self.enable_limit) 48 | 49 | self.Blue = port & 40 50 | self.Green = (port >> 8) & 40 51 | self.Red = (port >> 16) & 40 52 | with self.canvas.before: 53 | Color(self.Red/40.0, self.Green/40.0, self.Blue/40.0, .5) # green; colors range from 0-1 not 0-255 54 | self.rect = Rectangle(size=self.size, pos=self.pos) 55 | self.bind(size=self._update_rect, pos=self._update_rect) 56 | 57 | def _update_rect(self, instance, value): 58 | self.rect.pos = instance.pos 59 | self.rect.size = instance.size 60 | 61 | def update(self, v): 62 | self.net_data = v 63 | self.speed_label.text = "{:.1f}".format(v['speed'] / 1000.0) 64 | self.total_label.text = "{:.1f}".format(v['total'] / 1000.0) 65 | self.raw_speed_label.text = "{:.1f}".format(v['speed_raw'] / 1000.0) 66 | 67 | sort_key = 'speed' 68 | 69 | class TableHeader(BoxLayout): 70 | def __init__(self, **kwargs): 71 | super(TableHeader, self).__init__(**kwargs) 72 | 73 | self.port_label = Button(text="port", on_release=self.set_sort) 74 | self.port_label.sort_key = "port" 75 | self.total_label = Button(text="total\nkb", on_release=self.set_sort) 76 | self.total_label.sort_key = "total" 77 | self.raw_speed_label = Button(text="raw\nspd kb", on_release=self.set_sort) 78 | self.raw_speed_label.sort_key = "speed_raw" 79 | self.speed_label = Button(text="speed\nkb", on_release=self.set_sort) 80 | self.speed_label.sort_key = "speed" 81 | self.add_widget(self.port_label) 82 | self.add_widget(self.total_label) 83 | self.add_widget(self.raw_speed_label) 84 | self.add_widget(self.speed_label) 85 | 86 | self.up_limit_label = Label(text="up\nlimit") 87 | self.down_limit_label = Label(text="down\nlimit") 88 | self.enable_limit_label = Label(text="enable\nlimit") 89 | self.add_widget(self.up_limit_label) 90 | self.add_widget(self.down_limit_label) 91 | self.add_widget(self.enable_limit_label) 92 | 93 | self.size_hint_y = 0.1 94 | 95 | def set_sort(self, obj): 96 | global sort_key 97 | sort_key = obj.sort_key 98 | print(sort_key) 99 | 100 | 101 | '''def cmp_PI(ix, iy): 102 | x = ix.net_data[sort_key] 103 | y = iy.net_data[sort_key] 104 | if x > y: 105 | return 1 106 | elif x == y: 107 | return 0 108 | else: # x < y 109 | return -1''' 110 | 111 | 112 | class MainView(GridLayout): 113 | def __init__(self, **kwargs): 114 | kwargs['cols'] = 1 115 | super(MainView, self).__init__(**kwargs) 116 | 117 | self.main_table = BoxLayout(orientation='vertical') 118 | self.main_table.add_widget(TableHeader()) 119 | self.main_list = GridLayout(cols=1, spacing=1, row_default_height= '30dp', row_force_default= True, size_hint_y=None) 120 | self.main_list.bind(minimum_height=self.main_list.setter('height')) 121 | self.scroll_view = ScrollView() 122 | self.main_table.add_widget(self.scroll_view) 123 | self.scroll_view.add_widget(self.main_list) 124 | self.add_widget(Label(text="LinNetLim\n press apply to limit selected ports", size_hint_y=.15)) 125 | self.add_widget(self.main_table) 126 | 127 | Clock.schedule_interval(self.update_cb, 0.5) 128 | 129 | self.connected_widgets = {} 130 | self.info_panel = BoxLayout(orientation='vertical', size_hint_x=.2) 131 | 132 | applybtn = Button(text='Apply', size_hint_y=.1) 133 | applybtn.bind(on_release=self.apply_limits) 134 | self.add_widget(applybtn) 135 | unapplybtn = Button(text='Un-Apply', size_hint_y=.1) 136 | unapplybtn.bind(on_release=self.clear_limits) 137 | self.add_widget(unapplybtn) 138 | #self.info_panel.add_widget(clearbtn) 139 | 140 | #generate a list of ports with up/down limit and pass to the packet limiter 141 | def apply_limits(self, obj): 142 | indata = [] 143 | for r in self.main_list.children: 144 | if r.enable_limit.state == 'down': 145 | try: 146 | ul = int(r.up_limit.text) 147 | except: 148 | print("error, missing up limit, setting to 1000000") 149 | ul = 1000000 150 | try: 151 | dl = int(r.down_limit.text) 152 | except: 153 | print("error, missing down limit, setting to 1000000") 154 | dl = 1000000 155 | indata.append({ 156 | "port": int(r.port_label.text), 157 | "up_limit": ul, 158 | "down_limit": dl 159 | }) 160 | packet_limiter.set_from_ports_list(indata) 161 | 162 | 163 | def clear_limits(self, obj): 164 | packet_limiter.reset_all() 165 | 166 | #read list of ports, with raw_speed, total and speed 167 | def update_cb(self, dt): 168 | for k in list(packet_watcher.portcounts.keys()): 169 | v = packet_watcher.portcounts[k] 170 | w = self.connected_widgets.get(k, None) 171 | if not w: 172 | w = PortInfo(port=k, item=v, height=100) 173 | self.connected_widgets[k] = w 174 | self.main_list.add_widget(w) 175 | w.update(v) 176 | #list(self.main_list.children).sort(key=cmp_PI) 177 | super(self.main_list.children.__class__, self.main_list.children).sort(key=lambda child: child.net_data[sort_key]) 178 | 179 | 180 | class NetLimitApp(App): 181 | def build(self): 182 | parent = MainView() 183 | packet_watcher.start_background_thread() 184 | self.mainwidget = parent 185 | return parent 186 | 187 | 188 | mainapp = NetLimitApp() 189 | 190 | if __name__ == '__main__': 191 | mainapp.run() 192 | --------------------------------------------------------------------------------