├── shapy ├── emulation │ ├── __init__.py │ ├── commands │ │ ├── stats.py │ │ └── __init__.py │ └── shaper.py ├── framework │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ └── default.py │ ├── commands │ │ ├── ifb.py │ │ ├── teardown.py │ │ ├── tcclass.py │ │ ├── __init__.py │ │ ├── filter.py │ │ └── qdisc.py │ ├── tcelements.py │ ├── exceptions.py │ ├── mixin.py │ ├── netlink │ │ ├── __init__.py │ │ ├── prio.py │ │ ├── stats.py │ │ ├── netem.py │ │ ├── connection.py │ │ ├── tc.py │ │ ├── filter.py │ │ ├── constants.py │ │ ├── htb.py │ │ └── message.py │ ├── tcclass.py │ ├── qdisc.py │ ├── filter.py │ ├── utils.py │ ├── executor.py │ └── interface.py └── __init__.py ├── tests ├── emulation │ ├── __init__.py │ ├── settings.py │ ├── test_delay.py │ └── test_shaping.py ├── framework │ ├── __init__.py │ ├── test_interface.py │ ├── test_ingress.py │ ├── test_pfifo.py │ ├── test_settings.py │ ├── test_children.py │ ├── test_netem.py │ └── test_htb.py ├── netlink │ ├── __init__.py │ ├── htb_add_class.data │ ├── test_stats.py │ ├── test_qdiscs.py │ ├── test_privileges.py │ ├── test_htb_class.py │ ├── test_filter.py │ └── test_unpack.py ├── utils.py ├── __init__.py └── mixins.py ├── doc ├── thesis.pdf └── tc_filter_attr_structure.pdf ├── examples ├── settings.py ├── teardown.py └── example.py ├── .gitignore ├── LICENCE ├── setup.py └── README.markdown /shapy/emulation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shapy/framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/emulation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/netlink/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shapy/framework/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/thesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praus/shapy/HEAD/doc/thesis.pdf -------------------------------------------------------------------------------- /shapy/framework/commands/ifb.py: -------------------------------------------------------------------------------- 1 | IFB = "ip link set dev {interface!s} up" 2 | -------------------------------------------------------------------------------- /examples/settings.py: -------------------------------------------------------------------------------- 1 | EMU_INTERFACES = ('lo',) 2 | EMU_NOSHAPE_PORTS = ('8000',) 3 | 4 | -------------------------------------------------------------------------------- /shapy/emulation/commands/stats.py: -------------------------------------------------------------------------------- 1 | TCStats = "tc -s class show dev {interface} classid 1:{handle}" -------------------------------------------------------------------------------- /shapy/framework/commands/teardown.py: -------------------------------------------------------------------------------- 1 | TCInterfaceTeardown = "tc qdisc del dev {interface} {handle}" -------------------------------------------------------------------------------- /shapy/emulation/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from shapy.framework.commands import * 2 | from .stats import * -------------------------------------------------------------------------------- /doc/tc_filter_attr_structure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praus/shapy/HEAD/doc/tc_filter_attr_structure.pdf -------------------------------------------------------------------------------- /tests/netlink/htb_add_class.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praus/shapy/HEAD/tests/netlink/htb_add_class.data -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.komodoproject 3 | *.log* 4 | sudo_password.txt 5 | .project 6 | .pydevproject 7 | .settings 8 | /MANIFEST 9 | /dist 10 | /build 11 | -------------------------------------------------------------------------------- /shapy/framework/commands/tcclass.py: -------------------------------------------------------------------------------- 1 | 2 | HTBClass = """\ 3 | tc class add dev {interface} parent {parent} classid {handle} \ 4 | htb rate {rate}{units} ceil {ceil}{units}\ 5 | """ 6 | -------------------------------------------------------------------------------- /shapy/framework/tcelements.py: -------------------------------------------------------------------------------- 1 | from shapy.framework.qdisc import * 2 | from shapy.framework.tcclass import * 3 | from shapy.framework.filter import * 4 | from shapy.framework.interface import * 5 | -------------------------------------------------------------------------------- /shapy/framework/exceptions.py: -------------------------------------------------------------------------------- 1 | class ShapyException(Exception): 2 | """Base Shapy exception.""" 3 | pass 4 | 5 | class ImproperlyConfigured(Exception): 6 | # TODO: logging 7 | pass 8 | -------------------------------------------------------------------------------- /shapy/framework/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .filter import * 2 | from .qdisc import * 3 | from .ifb import * 4 | from .tcclass import * 5 | from .teardown import * 6 | 7 | # http://stackoverflow.com/questions/912025/how-to-find-all-child-modules-in-python 8 | # TODO: automatically import child modules -------------------------------------------------------------------------------- /tests/emulation/settings.py: -------------------------------------------------------------------------------- 1 | 2 | COMMANDS = 'shapy.emulation.commands' 3 | 4 | ### CWC settings ### 5 | 6 | # it's advisable to set MTU of the interfaces to something real, for example 1500 7 | EMU_INTERFACES = ( 8 | 'lo', 9 | ) 10 | 11 | # ports excluded from shaping and delaying (holds for in and out) 12 | # usually used for control ports 13 | EMU_NOSHAPE_PORTS = (8000,) 14 | -------------------------------------------------------------------------------- /shapy/framework/commands/filter.py: -------------------------------------------------------------------------------- 1 | 2 | RedirectFilter = """\ 3 | tc filter add dev {interface!s} parent {parent!s} \ 4 | protocol ip prio {prio} u32 \ 5 | match ip {ip_match} flowid 1:1 \ 6 | action mirred egress redirect dev {ifb!s}\ 7 | """ 8 | 9 | FlowFilter = """\ 10 | tc filter add dev {interface} parent {parent!s} \ 11 | protocol ip prio {prio} u32 \ 12 | match ip {ip_match} flowid {flowid}\ 13 | """ -------------------------------------------------------------------------------- /tests/netlink/test_stats.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from shapy.framework.tcelements import * 4 | from shapy.framework.netlink.stats import * 5 | 6 | 7 | class TestStats(unittest.TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def test_class_stats(self): 12 | data = get_stats(1, RTM_GETTCLASS) 13 | #print data.encode('string_escape') 14 | #print len(data) 15 | -------------------------------------------------------------------------------- /shapy/framework/settings/default.py: -------------------------------------------------------------------------------- 1 | try: 2 | with open('sudo_password.txt', 'r') as f: 3 | SUDO_PASSWORD = f.readlines()[0].strip() 4 | except IOError: 5 | SUDO_PASSWORD = '' 6 | 7 | # units can be either kbit meaning kilobits or kbps meaning kilobytes 8 | UNITS = 'kbps' 9 | 10 | ENV = { 11 | 'PATH': '/sbin:/bin:/usr/bin' 12 | } 13 | 14 | COMMANDS = 'shapy.framework.commands' 15 | 16 | HTB_DEFAULT_CLASS = 0x1ff 17 | -------------------------------------------------------------------------------- /shapy/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.0.1" 2 | 3 | import sys 4 | import shapy 5 | import shapy.framework.settings.default 6 | setattr(shapy, 'settings', sys.modules['shapy.framework.settings.default']) 7 | 8 | def register_settings(module_name): 9 | __import__(module_name) 10 | custom = sys.modules[module_name] 11 | for name in ( m for m in custom.__dict__ if not m.startswith('__') ): 12 | setattr(shapy.settings, name, getattr(custom, name)) 13 | -------------------------------------------------------------------------------- /tests/framework/test_interface.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shapy.framework.tcelements import * 3 | 4 | class TestInterface(unittest.TestCase): 5 | 6 | def test_interface_singleton(self): 7 | self.assertEqual(id(Interface('eth0')), id(Interface('eth0'))) 8 | self.assertNotEqual(id(Interface('eth0')), id(Interface('eth1'))) 9 | 10 | def test_ifindex(self): 11 | # loopback should always have if_index == 1 12 | self.assertEqual(Interface('lo').if_index, 1) -------------------------------------------------------------------------------- /tests/framework/test_ingress.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from shapy.framework.tcelements import * 4 | from shapy.framework.executor import run 5 | 6 | from tests import TCTestCase 7 | 8 | class TestIngress(TCTestCase): 9 | def setUp(self): 10 | self.interface = Interface('lo') 11 | 12 | def test_ingress_filter(self): 13 | q = IngressQdisc() 14 | q.add(RedirectFilter('dst 127.0.0.3', 'eth0')) 15 | self.interface.add_ingress(q) 16 | self.interface.set_shaping() 17 | -------------------------------------------------------------------------------- /tests/framework/test_pfifo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from shapy.framework.qdisc import pfifoQdisc 4 | from shapy.framework.interface import Interface 5 | from shapy.framework.executor import run 6 | 7 | from tests import TCTestCase 8 | 9 | class TestPfifo(TCTestCase): 10 | def setUp(self): 11 | self.interface = Interface('lo') 12 | self.pf = pfifoQdisc('1:') 13 | self.interface.add(self.pf) 14 | self.interface.set_shaping() 15 | 16 | def test_pfifo(self): 17 | out = run('tc qdisc show dev lo') 18 | 19 | def tearDown(self): 20 | self.interface.teardown() -------------------------------------------------------------------------------- /shapy/framework/commands/qdisc.py: -------------------------------------------------------------------------------- 1 | 2 | HTBRootQdisc = """\ 3 | tc qdisc add dev {interface!s} root handle 1: \ 4 | htb default {default_class!s}\ 5 | """ 6 | 7 | HTBQdisc = """\ 8 | tc qdisc add dev {interface!s} parent {parent!s} handle {handle!s} \ 9 | htb default {default_class!s}\ 10 | """ 11 | 12 | NetemDelayQdisc = """\ 13 | tc qdisc add dev {interface!s} parent {parent!s} handle {handle!s} \ 14 | netem delay {delay!s}ms\ 15 | """ 16 | 17 | IngressQdisc = "tc qdisc add dev {interface!s} ingress" 18 | 19 | PRIOQdisc = "tc qdisc add dev {interface!s} root handle 1: prio" 20 | 21 | pfifoQdisc = "tc qdisc add dev {interface!s} root handle 1: pfifo" -------------------------------------------------------------------------------- /tests/framework/test_settings.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import imp 3 | import sys 4 | import shapy 5 | 6 | class TestSettings(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.settings = imp.new_module('test_settings') 10 | sys.modules.update(test_settings=self.settings) 11 | setattr(self.settings, 'UNITS', 'override') 12 | setattr(self.settings, 'NEW_OPTION', 'new') 13 | 14 | def test_settings_override(self): 15 | shapy.register_settings('test_settings') 16 | from shapy import settings 17 | self.assertEqual(settings.UNITS, 'override') 18 | self.assertEqual(getattr(settings, 'NEW_OPTION', None), 'new') -------------------------------------------------------------------------------- /tests/framework/test_children.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shapy.framework.tcelements import * 3 | 4 | class TestChildrenManipulation(unittest.TestCase): 5 | def setUp(self): 6 | self.lo = Interface('lo') 7 | self.hq = HTBQdisc('1:', default_class='1ff') 8 | self.hc = HTBClass('1:1', rate=100, ceil=100) 9 | 10 | self.hq.add(self.hc) 11 | self.lo.add(self.hq) 12 | 13 | def test_add_class(self): 14 | self.assertEquals(self.hq, self.hc.parent) 15 | 16 | def test_add_interface(self): 17 | self.assertEquals(self.hq.interface, self.lo) 18 | 19 | def test_get_interface(self): 20 | self.assertEquals(self.hc.get_interface(), self.lo) 21 | -------------------------------------------------------------------------------- /tests/framework/test_netem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from shapy.framework.qdisc import * 4 | from shapy.framework.tcclass import * 5 | from shapy.framework.filter import * 6 | from shapy.framework.interface import * 7 | 8 | from tests.utils import ping 9 | from tests import TCTestCase 10 | 11 | class TestNetemDelay(TCTestCase): 12 | def setUp(self): 13 | super(TestNetemDelay, self).setUp() 14 | self.delay = 50 # ms 15 | self.interface = Interface('lo') 16 | self.interface.add(NetemDelayQdisc('1:', self.delay)) 17 | self.interface.set_shaping() 18 | 19 | def test_delay(self): 20 | c = "127.0.0.2" 21 | s = "127.0.0.3" 22 | ping(c, s, 1) # ARP 23 | self.assertAlmostEqual(ping(c, s), self.delay*2, delta=2) 24 | -------------------------------------------------------------------------------- /shapy/framework/mixin.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ChildrenMixin(object): 4 | def __init__(self): 5 | self.children = [] 6 | 7 | def add(self, child): 8 | child.parent = self 9 | self.children.append(child) 10 | 11 | class FilterMixin(ChildrenMixin): 12 | def add(self, child): 13 | from shapy.framework.filter import Filter 14 | assert isinstance(child, Filter), \ 15 | "%s can contain only filters." % self.__class__.__name__ 16 | ChildrenMixin.add(self, child) 17 | 18 | class ClassFilterMixin(ChildrenMixin): 19 | def add(self, child): 20 | from shapy.framework.filter import Filter 21 | from shapy.framework.tcclass import TCClass 22 | assert isinstance(child, (TCClass, Filter)), \ 23 | "%s can contain only filters or classes, not qdiscs." % self.__class__.__name__ 24 | ChildrenMixin.add(self, child) 25 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Petr Praus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /shapy/framework/netlink/__init__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from shapy.framework.executor import Executable 3 | from .connection import Connection 4 | from .message import Message, Attr 5 | from .tc import tcmsg 6 | from .constants import * 7 | from shapy.framework.utils import convert_handle 8 | 9 | connection = Connection() 10 | 11 | class NetlinkExecutable(Executable): 12 | def execute(self): 13 | c = self.get_context() 14 | interface = self.get_interface() 15 | handle = convert_handle(self['handle']) 16 | 17 | try: 18 | parent = convert_handle(c['parent']) 19 | except ValueError: 20 | parent = TC_H_ROOT 21 | 22 | info = getattr(self, 'tcm_info', 0) 23 | tcm = tcmsg(socket.AF_UNSPEC, interface.if_index, handle, parent, info, 24 | self.attrs) 25 | msg = Message(type=self.type, 26 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 27 | service_template=tcm) 28 | 29 | connection.send(msg) 30 | resp = connection.recv() 31 | return resp 32 | -------------------------------------------------------------------------------- /tests/emulation/test_delay.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import SocketServer, socket 3 | from tests.utils import ping 4 | from tests.mixins import ShaperMixin 5 | import time 6 | 7 | from shapy import register_settings 8 | register_settings('tests.emulation.settings') 9 | 10 | class TestCWCDelay(unittest.TestCase, ShaperMixin): 11 | def setUp(self): 12 | self.server_addr = ('127.0.0.2', 55000) # 5 ms delay 13 | self.client_addr = ('127.0.0.3', 55001) # 30 ms delay 14 | ShaperMixin.setUp(self) 15 | 16 | def test_delay(self): 17 | c = self.client_addr[0] 18 | s = self.server_addr[0] 19 | ping(c, s, 1) # ARP 20 | est_delay = self.estimate_delay(c, s) 21 | 22 | self.assertAlmostEqual(ping(c, s), est_delay*2, delta=1) 23 | self.assertAlmostEqual(ping(s, c), est_delay*2, delta=1) 24 | self.assertAlmostEqual(ping(c, "127.0.0.1"), 60, delta=0.5) 25 | self.assertAlmostEqual(ping("127.0.0.1", c), 60, delta=0.5) 26 | self.assertAlmostEqual(ping(s, "127.0.0.1"), 10, delta=1) 27 | self.assertAlmostEqual(ping("127.0.0.1", s), 10, delta=1) 28 | 29 | def tearDown(self): 30 | ShaperMixin.tearDown(self) -------------------------------------------------------------------------------- /examples/teardown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | logging.basicConfig(level=logging.INFO, datefmt='%H:%M:%S', 5 | format='%(asctime)s %(levelname)s: %(message)s') 6 | 7 | def scan_interfaces(): 8 | """Parses a list of all interfaces reported by `ip link`""" 9 | import subprocess 10 | import re 11 | ifcs = [] 12 | out = subprocess.check_output(["ip", "link"]).split('\n') 13 | for line in out: 14 | m = re.match("^[0-9]+:[ ]([a-z0-9]+):", line) 15 | if m: 16 | ifcs.append(m.group(1)) 17 | return ifcs 18 | 19 | 20 | from shapy import register_settings 21 | register_settings('settings') 22 | from shapy.emulation.shaper import Shaper 23 | 24 | from shapy import settings 25 | settings.EMU_INTERFACES = scan_interfaces() 26 | 27 | notice = """\ 28 | This will clear entire Traffic Control configuration and unload IFB module. 29 | Please note reported errors usually do not mean anything bad, just that there 30 | was nothing to tear down on that particular interface.""" 31 | 32 | if __name__ == '__main__': 33 | print "Tearing down all interfaces: %s" % ', '.join(settings.EMU_INTERFACES) 34 | print notice 35 | print "-"*80 36 | sh = Shaper() 37 | sh.teardown_all() 38 | -------------------------------------------------------------------------------- /shapy/framework/tcclass.py: -------------------------------------------------------------------------------- 1 | from shapy.framework.netlink import NetlinkExecutable 2 | from shapy.framework.netlink.constants import * 3 | from shapy.framework.netlink.message import * 4 | from shapy.framework.netlink.htb import * 5 | 6 | from shapy.framework.mixin import ChildrenMixin 7 | from shapy.framework.utils import convert_to_bytes 8 | from shapy import settings 9 | 10 | class TCClass(NetlinkExecutable, ChildrenMixin): 11 | type = RTM_NEWTCLASS 12 | 13 | def __init__(self, handle, **kwargs): 14 | NetlinkExecutable.__init__(self, **kwargs) 15 | ChildrenMixin.__init__(self) 16 | self.opts.update({'handle': handle}) 17 | 18 | class HTBClass(TCClass): 19 | def __init__(self, handle, rate, ceil=0, mtu=1600): 20 | if not ceil: ceil = rate 21 | rate = convert_to_bytes(rate) 22 | ceil = convert_to_bytes(ceil) 23 | init = Attr(TCA_HTB_INIT, 24 | HTBParms(rate, ceil).pack() + 25 | RTab(rate, mtu).pack() + CTab(ceil, mtu).pack()) 26 | self.attrs = [Attr(TCA_KIND, 'htb\0'), init] 27 | TCClass.__init__(self, handle) 28 | 29 | def get_context(self): 30 | c = TCClass.get_context(self) 31 | c.update({'units': settings.UNITS}) 32 | return c -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | logging.basicConfig(level=logging.INFO, datefmt='%H:%M:%S', 5 | format='%(asctime)s %(name)s %(levelname)s: %(message)s') 6 | 7 | from shapy import register_settings 8 | register_settings('settings') 9 | from shapy.emulation.shaper import Shaper 10 | 11 | if __name__ == '__main__': 12 | ps = {("127.0.0.2",) : {'upload': 1024, 'download': 1024, 'delay': 5, 'jitter': 5}, 13 | ("127.0.0.3",) : {'upload': 256, 'download': 512, 'delay': 20}, 14 | ("127.0.0.4",) : {'upload': 256, 'download': 512, 'delay': 20}, 15 | } 16 | 17 | sh = Shaper() 18 | sh.set_shaping(ps) 19 | 20 | #print sh.ip_handles 21 | 22 | #sh.teardown_all() 23 | 24 | #up, down = sh.get_traffic("127.0.0.2") 25 | #print up, down 26 | 27 | 28 | # Please be aware that second call to set_shaping with the same IP will not change the settings 29 | 30 | # How to generate large range of IP adresses: 31 | #ps = {tuple([ "127.0.0.%d" % x for x in range(1,250) ]) : {'upload': 256, 'download': 1024, 'delay': 25}, 32 | # } 33 | # 34 | # sh.set_shaping(ps) 35 | 36 | # Reuse of the same instance: 37 | # sh.reset_all() 38 | # 39 | # ps = {("127.0.0.2",) : {'upload': 512, 'download': 512, 'delay': 5}, 40 | # } 41 | # sh.set_shaping(ps) 42 | 43 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import shlex 3 | 4 | class PingError(OSError): 5 | pass 6 | 7 | def ping(host_from, host_to, count=4, interval=0.2): 8 | """ 9 | ICMP packets has to be sent by a process owned by the root, /bin/ping has 10 | setuid so I'm using this to avoid juggling with root privileges. 11 | This is blocking. 12 | """ 13 | assert interval>=0.2, "Interval cannot be less than 0.2 seconds" 14 | cmd = 'ping -c {count} -I {0} -i {interval} {1}'\ 15 | .format(host_from, host_to, interval=interval, count=count) 16 | p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, 17 | stderr=subprocess.PIPE) 18 | stdout, stderr = p.communicate() 19 | if p.returncode == 0: 20 | return float(stdout.splitlines()[-1].split('/')[4]) 21 | raise PingError(p.returncode) 22 | 23 | 24 | def eta(filesize, a, b): 25 | """ 26 | Determines how long the transfer of filesize bytes from A to B should take. 27 | units for filesize are bytes, a and b is speed in kilobytes 28 | """ 29 | return filesize/(min(a, b)*1024) 30 | 31 | def total_seconds(td): 32 | """ 33 | http://docs.python.org/library/datetime.html#datetime.timedelta.total_seconds 34 | """ 35 | return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6) 36 | 37 | 38 | def random_file(size): 39 | with open('/dev/urandom', 'rb') as f: 40 | return bytearray(f.read(size)) 41 | 42 | def hex_list(lst): 43 | return [ "{0:#x}".format(a) for a in lst ] -------------------------------------------------------------------------------- /shapy/framework/netlink/prio.py: -------------------------------------------------------------------------------- 1 | from shapy.framework.netlink.message import Attr 2 | from .constants import * 3 | from struct import Struct 4 | from collections import namedtuple 5 | 6 | 7 | class PrioQdiscAttr(Attr): 8 | #define TC_PRIO_BESTEFFORT 0 9 | #define TC_PRIO_FILLER 1 10 | #define TC_PRIO_BULK 2 11 | #define TC_PRIO_INTERACTIVE_BULK 4 12 | #define TC_PRIO_INTERACTIVE 6 13 | #define TC_PRIO_CONTROL 7 14 | 15 | #define TC_PRIO_MAX 15 16 | 17 | #define TCQ_PRIO_BANDS 16 18 | #define TCQ_MIN_PRIO_BANDS 2 19 | 20 | #struct tc_prio_qopt { 21 | # int bands; /* Number of bands */ 22 | # __u8 priomap[TC_PRIO_MAX+1]; /* Map: logical priority -> PRIO band */ 23 | #}; 24 | 25 | data_format = Struct("i16B4x") 26 | data_struct = namedtuple('tc_prio_qopt', "bands priomap") 27 | 28 | @classmethod 29 | def unpack(cls, data): 30 | attr, rest = Attr.unpack(data) 31 | data = self.data_format.unpack(attr.payload) 32 | opts = cls.data_struct._make((data[0], data[1:])) 33 | return cls(opts.bands, opts.priomap), rest 34 | 35 | def unpack_data(self): 36 | data = self.data_format.unpack(self.payload) 37 | return self.data_struct._make(self.data_struct._make((data[0], data[1:]))) 38 | 39 | def __init__(self, bands=3, priomap="1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1"): 40 | pm = [ int(p) for p in priomap.split() ] 41 | data = self.data_format.pack(bands, *pm) 42 | Attr.__init__(self, TCA_OPTIONS, data) -------------------------------------------------------------------------------- /tests/netlink/test_qdiscs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | import time 4 | 5 | from shapy.framework.netlink.constants import * 6 | from shapy.framework.netlink.message import * 7 | from shapy.framework.netlink.tc import * 8 | from shapy.framework.netlink.connection import Connection 9 | from shapy.framework.netlink.htb import HTBQdiscAttr 10 | from shapy.framework.netlink.netem import NetemOptions 11 | from shapy.framework.utils import nl_us2ticks 12 | 13 | from tests import TCTestCase 14 | 15 | class TestQdisc(TCTestCase): 16 | def setUp(self): 17 | self.if_index = 1 # lo 18 | self.qhandle = 0x1 << 16 # | 0x1 # major:minor 19 | super(TestQdisc, self).setUp() 20 | 21 | def test_add_pfifo_qdisc(self): 22 | self.make_msg([Attr(TCA_KIND, 'pfifo\0')]) 23 | 24 | def test_add_htb_qdisc(self): 25 | self.make_msg([Attr(TCA_KIND, 'htb\0'), HTBQdiscAttr(defcls=0x1ff)]) 26 | 27 | def test_add_netem_qdisc(self): 28 | delay = nl_us2ticks(500*1000) # 500 ms 29 | self.make_msg([Attr(TCA_KIND, 'netem\0'), NetemOptions(delay)]) 30 | 31 | def make_msg(self, attrs): 32 | """Creates and sends a qdisc-creating message and service template.""" 33 | tcm = tcmsg(socket.AF_UNSPEC, self.if_index, self.qhandle, TC_H_ROOT, 0, 34 | attrs) 35 | msg = Message(type=RTM_NEWQDISC, 36 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 37 | service_template=tcm) 38 | self.conn.send(msg) 39 | self.check_ack(self.conn.recv()) 40 | 41 | -------------------------------------------------------------------------------- /shapy/framework/netlink/stats.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from struct import Struct 3 | 4 | from .connection import Connection 5 | from .message import Message, Attr 6 | from .tc import tcmsg 7 | from .constants import * 8 | 9 | from shapy.framework.utils import convert_handle 10 | 11 | # RTM_GETTCLASS 12 | 13 | def get_stats(if_index, type): 14 | #struct tc_stats { 15 | # __u64 bytes; /* NUmber of enqueues bytes */ 16 | # __u32 packets; /* Number of enqueued packets */ 17 | # __u32 drops; /* Packets dropped because of lack of resources */ 18 | # __u32 overlimits; /* Number of throttle events when this 19 | # * flow goes out of allocated bandwidth */ 20 | # __u32 bps; /* Current flow byte rate */ 21 | # __u32 pps; /* Current flow packet rate */ 22 | # __u32 qlen; 23 | # __u32 backlog; 24 | #}; 25 | 26 | tc_stats = Struct("L7I") 27 | 28 | #handle = 0x1 << 16 | 0x1 29 | handle = 0x0 30 | tcm = tcmsg(socket.AF_UNSPEC, if_index, handle, TC_H_ROOT, 0, 31 | attributes=[]) 32 | msg = Message(type=type, 33 | flags=NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST, 34 | service_template=tcm) 35 | 36 | conn = Connection() 37 | conn.send(msg) 38 | 39 | msgs = [] 40 | flags = NLM_F_MULTI 41 | while flags == NLM_F_MULTI: 42 | m = conn.recv() 43 | msgs.append(m) 44 | flags = m.flags 45 | if m.type == NLMSG_DONE: 46 | break 47 | 48 | return msgs 49 | 50 | 51 | -------------------------------------------------------------------------------- /shapy/framework/netlink/netem.py: -------------------------------------------------------------------------------- 1 | 2 | from struct import Struct 3 | from collections import namedtuple 4 | 5 | from shapy.framework.netlink.message import Attr 6 | from shapy.framework.utils import nl_ticks2us, nl_us2ticks 7 | from shapy.framework.netlink.constants import * 8 | 9 | 10 | 11 | class NetemOptions(Attr): 12 | #struct tc_netem_qopt { 13 | # __u32 latency; /* added delay (us) */ 14 | # __u32 limit; /* fifo limit (packets) */ 15 | # __u32 loss; /* random packet loss (0=none ~0=100%) */ 16 | # __u32 gap; /* re-ordering gap (0 for none) */ 17 | # __u32 duplicate; /* random packet dup (0=none ~0=100%) */ 18 | # __u32 jitter; /* random jitter in latency (us) */ 19 | #}; 20 | 21 | data_format = Struct("6I") 22 | data_struct = namedtuple('tc_netem_qopt', 23 | "latency limit loss gap duplicate jitter") 24 | 25 | @classmethod 26 | def unpack(cls, data): 27 | attr, rest = Attr.unpack(data) 28 | opts = cls.data_struct._make(cls.data_format.unpack(attr.payload)) 29 | opts = opts._replace(latency=nl_ticks2us(opts.latency)) 30 | return cls(*opts), rest 31 | 32 | def __init__(self, latency, limit=1000, loss=0, gap=0, duplicate=0, jitter=0): 33 | """Latency is in microseconds [us]""" 34 | latency_ticks = nl_us2ticks(latency) 35 | jitter_ticks = nl_us2ticks(jitter) 36 | data = self.data_format.pack(latency_ticks, limit, loss, 37 | gap, duplicate, jitter_ticks) 38 | Attr.__init__(self, TCA_OPTIONS, data) 39 | -------------------------------------------------------------------------------- /shapy/framework/netlink/connection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import struct 4 | from .constants import * 5 | from .message import Message 6 | 7 | class Connection(object): 8 | """ 9 | Object representing Netlink socket connection to the kernel. 10 | """ 11 | def __init__(self, nlservice=socket.NETLINK_ROUTE, groups=0): 12 | # nlservice = Netlink IP service 13 | self.fd = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, nlservice) 14 | self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536) 15 | self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536) 16 | self.fd.bind((0, groups)) # pid=0 lets kernel assign socket PID 17 | self.pid, self.groups = self.fd.getsockname() 18 | self._seq = 0 19 | 20 | def send(self, msg): 21 | if isinstance(msg, Message): 22 | if msg.seq == -1: 23 | msg.seq = self.seq() 24 | msg.pid = self.pid 25 | msg = msg.pack() 26 | self.fd.send(msg) 27 | 28 | def recv(self): 29 | contents, (nlpid, nlgrps) = self.fd.recvfrom(16384) 30 | msg = Message.unpack(contents) 31 | 32 | if msg.type == NLMSG_ERROR: 33 | errno = -msg.service_template.error_code 34 | #errno = -struct.unpack("i", msg.payload[:4])[0] 35 | if errno != 0: 36 | err = OSError("Netlink error: %s (%d) | msg: %s" % ( 37 | os.strerror(errno), errno, msg.service_template.old_message)) 38 | err.errno = errno 39 | raise err 40 | return msg 41 | 42 | def seq(self): 43 | self._seq += 1 44 | return self._seq -------------------------------------------------------------------------------- /tests/netlink/test_privileges.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os, pwd, grp 3 | import socket 4 | 5 | from shapy.framework.netlink.constants import * 6 | 7 | @unittest.skip("This is just an illustration simply dropping privileges does not work.") 8 | class TestPrivilegeDrop(unittest.TestCase): 9 | def setUp(self): 10 | self.if_index = 1 11 | self.qhandle = 0x1 << 16 12 | 13 | def test_drop(self): 14 | self.assertEqual(os.getuid(), pwd.getpwnam("root").pw_uid, 15 | "You have to be root to run this test") 16 | 17 | # import netlink a create a connection as root 18 | from shapy.framework import netlink 19 | 20 | # http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python 21 | # drop privileges to nobody 22 | os.setuid(pwd.getpwnam("nobody").pw_uid) 23 | #os.setgid(grp.getgrnam("nogroup").gr_gid) 24 | #os.setgroups([]) 25 | 26 | # try to do operation on a netlink socket while being an unprivileged user 27 | from shapy.framework.netlink.message import Message, Attr 28 | from shapy.framework.netlink.tc import tcmsg 29 | 30 | tcm = tcmsg(socket.AF_UNSPEC, self.if_index, self.qhandle, TC_H_ROOT, 0, 31 | [Attr(TCA_KIND, 'pfifo\0')]) 32 | msg = Message(type=RTM_NEWQDISC, 33 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 34 | service_template=tcm) 35 | 36 | netlink.connection.send(msg) 37 | 38 | # and observe we are out of luck :/ 39 | self.assertRaisesRegexp(OSError, "Operation not permitted", 40 | netlink.connection.recv) 41 | -------------------------------------------------------------------------------- /tests/framework/test_htb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from shapy.framework.qdisc import * 4 | from shapy.framework.tcclass import * 5 | from shapy.framework.filter import * 6 | from shapy.framework.interface import * 7 | 8 | from tests.utils import ping, random_file, eta 9 | from tests.mixins import ServerMixin, ClientMixin 10 | 11 | class TestHTB(unittest.TestCase, ServerMixin, ClientMixin): 12 | def setUp(self): 13 | self.server_addr = ('127.0.0.2', 55000) 14 | self.client_addr = ('127.0.0.3', 55001) 15 | self.server_speed = 256 # download speed in kbyte/s 16 | self.client_speed = 128 17 | 18 | self.i = Interface('lo') 19 | try: 20 | self.i.teardown() 21 | except OSError: 22 | pass 23 | h1 = HTBQdisc('1:')#, default_class='1ff') 24 | h1.add( FlowFilter('dst 127.0.0.2', '1:2', prio=1) ) 25 | h1.add( FlowFilter('dst 127.0.0.3', '1:1', prio=1) ) 26 | h1.add( HTBClass('1:1', rate=self.server_speed, ceil=self.server_speed)) 27 | h1.add( HTBClass('1:2', rate=self.client_speed, ceil=self.client_speed)) 28 | self.i.add( h1 ) 29 | self.i.set_shaping() 30 | 31 | self.filesize = 2**20 32 | self.randomfile = random_file(self.filesize) 33 | self.sleep = 4 34 | 35 | self.run_server() 36 | 37 | def test_shaping(self): 38 | time_up, time_down = self.make_transfer() 39 | 40 | eta = self.filesize/(self.client_speed*1000) 41 | self.assertAlmostEqual(time_up, eta, delta=2) 42 | 43 | eta = self.filesize/(self.server_speed*1000) 44 | self.assertAlmostEqual(time_down, eta, delta=1) 45 | 46 | def tearDown(self): 47 | self.i.teardown() 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | 4 | from shapy.framework.netlink.connection import Connection 5 | from shapy.framework.netlink.message import * 6 | from shapy.framework.netlink.tc import * 7 | from shapy.framework.netlink.constants import * 8 | from shapy.framework.interface import Interface 9 | 10 | class TCTestCase(unittest.TestCase): 11 | """ 12 | Generic test case used as a base for testing any Traffic Control components. 13 | """ 14 | conn = Connection() 15 | 16 | def setUp(self): 17 | self.interface = Interface('lo') 18 | # clean the environment before we start 19 | self.tearDown() 20 | 21 | def delete_root_qdisc(self): 22 | """ 23 | Deletes root egress qdisc on a interface designated by self.if_index 24 | and returns the resulting ACK message. 25 | """ 26 | if_index = getattr(self, 'if_index', self.interface.if_index) 27 | tm = tcmsg(socket.AF_UNSPEC, if_index, 0, TC_H_ROOT, 0) 28 | return self._del_qdisc(tm) 29 | 30 | def delete_ingress_qdisc(self): 31 | """ 32 | Deletes ingress qdisc on a interface designated by self.if_index 33 | and returns the resulting ACK message. 34 | """ 35 | if_index = getattr(self, 'if_index', self.interface.if_index) 36 | tm = tcmsg(socket.AF_UNSPEC, if_index, 0, TC_H_INGRESS, 0) 37 | return self._del_qdisc(tm) 38 | 39 | def _del_qdisc(self, st): 40 | msg = Message(type=RTM_DELQDISC, 41 | flags=NLM_F_REQUEST | NLM_F_ACK, 42 | service_template=st) 43 | self.conn.send(msg) 44 | return self.conn.recv() 45 | 46 | def check_ack(self, ack): 47 | self.assertIsInstance(ack.service_template, ACK) 48 | self.assertEquals(ack.type, 2) 49 | self.assertEquals(ack.flags, 0x0) 50 | 51 | def tearDown(self): 52 | try: 53 | self.delete_root_qdisc() 54 | self.delete_ingress_qdisc() 55 | except OSError: 56 | pass -------------------------------------------------------------------------------- /tests/netlink/test_htb_class.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | import os 4 | from shapy.framework.netlink.constants import * 5 | from shapy.framework.netlink.message import * 6 | from shapy.framework.netlink.tc import * 7 | from shapy.framework.netlink.htb import * 8 | from shapy.framework.netlink.connection import Connection 9 | 10 | from tests import TCTestCase 11 | 12 | class TestClass(TCTestCase): 13 | 14 | def test_add_class(self): 15 | self.qhandle = 0x1 << 16 # | 0x1 # major:minor, 1: 16 | self.add_htb_qdisc() 17 | 18 | handle = 0x1 << 16 | 0x1 19 | rate = 256*1000 20 | mtu = 1600 21 | 22 | this_dir = os.path.dirname(os.path.realpath(__file__)) 23 | with open(os.path.join(this_dir, 'htb_add_class.data'), 'rb') as f: 24 | data = f.read() 25 | 26 | #init = Attr(TCA_HTB_INIT, HTBParms(rate, rate).pack()+data[36+8+4+48:]) 27 | 28 | init = Attr(TCA_HTB_INIT, 29 | HTBParms(rate, rate).pack() + 30 | RTab(rate, mtu).pack() + CTab(rate, mtu).pack()) 31 | 32 | tcm = tcmsg(socket.AF_UNSPEC, self.interface.if_index, handle, self.qhandle, 0, 33 | [Attr(TCA_KIND, 'htb\0'), init]) 34 | 35 | msg = Message(type=RTM_NEWTCLASS, 36 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 37 | service_template=tcm) 38 | 39 | self.conn.send(msg) 40 | self.check_ack(self.conn.recv()) 41 | 42 | self.delete_root_qdisc() 43 | 44 | def add_htb_qdisc(self): 45 | tcm = tcmsg(socket.AF_UNSPEC, self.interface.if_index, self.qhandle, TC_H_ROOT, 0, 46 | [Attr(TCA_KIND, 'htb\0'), HTBQdiscAttr(defcls=0x1ff)]) 47 | 48 | msg = Message(type=RTM_NEWQDISC, 49 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 50 | service_template=tcm) 51 | 52 | self.conn.send(msg) 53 | r = self.conn.recv() 54 | self.check_ack(r) 55 | return r 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import os 5 | import sys 6 | 7 | def fullsplit(path, result=None): 8 | """ 9 | Split a pathname into components (the opposite of os.path.join) in a 10 | platform-neutral way. 11 | """ 12 | if result is None: 13 | result = [] 14 | head, tail = os.path.split(path) 15 | if head == '': 16 | return [tail] + result 17 | if head == path: 18 | return result 19 | return fullsplit(head, [tail] + result) 20 | 21 | # Compile the list of packages available, because distutils doesn't have 22 | # an easy way to do this. 23 | packages, data_files = [], [] 24 | root_dir = os.path.dirname(__file__) 25 | if root_dir != '': 26 | os.chdir(root_dir) 27 | project_dir = 'shapy' 28 | 29 | for dirpath, dirnames, filenames in os.walk(project_dir): 30 | # Ignore dirnames that start with '.' 31 | for i, dirname in enumerate(dirnames): 32 | if dirname.startswith('.'): del dirnames[i] 33 | if '__init__.py' in filenames: 34 | packages.append('.'.join(fullsplit(dirpath))) 35 | elif filenames: 36 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) 37 | 38 | def get_version(): 39 | import shapy 40 | return shapy.VERSION 41 | 42 | setup(name='ShaPy', 43 | version=get_version(), 44 | description='Netlink and network emulation framework', 45 | author='Petr Praus', 46 | author_email='petr@praus.net', 47 | url='https://github.com/praus/shapy', 48 | packages=packages, 49 | provides=['shapy'], 50 | classifiers=[ 51 | 'Development Status :: 4 - Beta', 52 | 'Environment :: Console', 53 | 'Intended Audience :: Science/Research', 54 | 'License :: OSI Approved :: GNU General Public License (GPL)', 55 | 'Natural Language :: English', 56 | 'Operating System :: POSIX :: Linux', 57 | 'Programming Language :: Python :: 2.6', 58 | 'Programming Language :: Python :: 2.7', 59 | 'Topic :: System', 60 | 'Topic :: System :: Emulators', 61 | 'Topic :: System :: Networking', 62 | 'Topic :: Software Development :: Libraries :: Python Modules', 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /shapy/framework/netlink/tc.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | from .message import ServiceTemplate 3 | from .constants import * 4 | 5 | class tcmsg(ServiceTemplate): 6 | """ 7 | Traffic Control service template 8 | http://tools.ietf.org/html/rfc3549#page-21 9 | """ 10 | #0 1 2 3 11 | #0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 12 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 13 | #| Family | Reserved1 | Reserved2 | 14 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15 | #| Interface Index | 16 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17 | #| Qdisc handle | 18 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | #| Parent Qdisc | 20 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | #| TCM Info | 22 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 23 | 24 | #struct tcmsg { 25 | # unsigned char tcm_family; 26 | # int tcm_ifindex; /* interface index */ 27 | # __u32 tcm_handle; /* Qdisc handle */ 28 | # __u32 tcm_parent; /* Parent qdisc */ 29 | # __u32 tcm_info; 30 | #}; 31 | 32 | format = Struct("BxxxiIII") 33 | 34 | def __init__(self, tcm_family, tcm_ifindex, tcm_handle, tcm_parent, tcm_info, 35 | attributes=[]): 36 | self.tcm_family = tcm_family 37 | self.tcm_ifindex = tcm_ifindex 38 | self.tcm_handle = tcm_handle 39 | self.tcm_parent = tcm_parent 40 | self.tcm_info = tcm_info 41 | self.attributes = attributes 42 | 43 | def pack(self): 44 | tm = tcmsg.format.pack(self.tcm_family, self.tcm_ifindex, 45 | self.tcm_handle, self.tcm_parent, self.tcm_info) 46 | return tm + self.pack_attrs() 47 | 48 | def __repr__(self): 49 | return "".format( 50 | self.tcm_family, self.tcm_ifindex, self.tcm_handle, 51 | self.tcm_parent, self.tcm_info) 52 | 53 | 54 | ServiceTemplate.register(tcmsg, (RTM_NEWQDISC, RTM_DELQDISC, RTM_GETQDISC, 55 | RTM_NEWTCLASS, RTM_DELTCLASS, RTM_GETTCLASS, 56 | RTM_NEWTFILTER, RTM_DELTFILTER, RTM_GETTFILTER)) 57 | -------------------------------------------------------------------------------- /shapy/framework/qdisc.py: -------------------------------------------------------------------------------- 1 | from shapy.framework.executor import Executable, get_command 2 | from shapy.framework.mixin import ChildrenMixin, FilterMixin, ClassFilterMixin 3 | from shapy.framework.filter import Filter 4 | from shapy import settings 5 | 6 | from shapy.framework.netlink.constants import * 7 | from shapy.framework.netlink import NetlinkExecutable 8 | from shapy.framework.netlink.message import Attr 9 | from shapy.framework.netlink.htb import HTBQdiscAttr 10 | from shapy.framework.netlink.netem import NetemOptions 11 | from shapy.framework.netlink.prio import PrioQdiscAttr 12 | 13 | class Qdisc(NetlinkExecutable): 14 | type = RTM_NEWQDISC 15 | attrs = [] 16 | 17 | def __init__(self, handle, **kwargs): 18 | Executable.__init__(self, **kwargs) 19 | self.opts.update({'handle': handle}) 20 | 21 | 22 | class QdiscClassless(Qdisc, FilterMixin): 23 | def __init__(self, *args, **kwargs): 24 | Qdisc.__init__(self, *args, **kwargs) 25 | FilterMixin.__init__(self) 26 | 27 | class pfifoQdisc(QdiscClassless): 28 | attrs = [Attr(TCA_KIND, 'pfifo\0')] 29 | 30 | class IngressQdisc(QdiscClassless): 31 | attrs = [Attr(TCA_KIND, 'ingress\0')] 32 | 33 | def __init__(self, handle='ffff:', **kwargs): 34 | QdiscClassless.__init__(self, handle, **kwargs) 35 | 36 | def get_context(self): 37 | return {'interface': self.interface, 38 | 'parent': TC_H_INGRESS} 39 | 40 | class NetemDelayQdisc(QdiscClassless): 41 | def __init__(self, handle, latency, jitter, **kwargs): 42 | self.attrs = [Attr(TCA_KIND, 'netem\0'), 43 | NetemOptions(latency*1000, jitter=jitter*1000)] 44 | QdiscClassless.__init__(self, handle, **kwargs) 45 | 46 | 47 | class QdiscClassful(Qdisc, ClassFilterMixin): 48 | def __init__(self, *args, **kwargs): 49 | Qdisc.__init__(self, *args, **kwargs) 50 | ChildrenMixin.__init__(self) 51 | 52 | class HTBQdisc(QdiscClassful): 53 | attrs = [Attr(TCA_KIND, 'htb\0'), 54 | HTBQdiscAttr(defcls=settings.HTB_DEFAULT_CLASS)] 55 | #def get(self): 56 | # """ 57 | # A slightly more complicated get method to distinguish between root 58 | # qdisc and normal qdisc. 59 | # """ 60 | # self.opts.update(self.get_context()) 61 | # if hasattr(self, 'interface'): 62 | # return get_command('HTBRootQdisc', interface=self.interface, 63 | # default_class=settings.HTB_DEFAULT_CLASS) 64 | # 65 | # return self.cmd.format(**self.opts) 66 | 67 | class PRIOQdisc(QdiscClassful): 68 | attrs = [Attr(TCA_KIND, 'prio\0'), PrioQdiscAttr()] 69 | -------------------------------------------------------------------------------- /tests/netlink/test_filter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | from shapy.framework.netlink.constants import * 4 | from shapy.framework.netlink.message import * 5 | from shapy.framework.netlink.tc import * 6 | from shapy.framework.netlink.htb import HTBQdiscAttr 7 | from shapy.framework.netlink.filter import * 8 | from shapy.framework.netlink.connection import Connection 9 | 10 | from tests import TCTestCase 11 | 12 | class TestFilter(TCTestCase): 13 | """ 14 | Tests adding TC filters using netlink interface. 15 | """ 16 | 17 | def setUp(self): 18 | super(TestFilter, self).setUp() 19 | self.qhandle = 0x1 << 16 # | 0x1 # major:minor, 1: 20 | ip_packed = socket.inet_aton("127.0.0.3") 21 | self.selector = u32_selector(val=struct.unpack("I", ip_packed)[0], 22 | offset=12) 23 | self.classid = u32_classid(0x10003) 24 | 25 | prio = 13 26 | protocol = ETH_P_IP 27 | self.tcm_info = prio << 16 | protocol 28 | 29 | def test_add_filter(self): 30 | self.add_htb_qdisc() 31 | 32 | opts = Attr(TCA_OPTIONS, self.classid.pack() + self.selector.pack()) 33 | self.add_filter(self.tcm_info, [Attr(TCA_KIND, 'u32\0'), opts]) 34 | 35 | 36 | def test_add_redirect_filter(self): 37 | self.add_htb_qdisc() 38 | 39 | action = u32_mirred_action(self.interface.if_index) 40 | opts = Attr(TCA_OPTIONS, 41 | self.classid.pack() + action.pack() + self.selector.pack()) 42 | self.add_filter(self.tcm_info, [Attr(TCA_KIND, 'u32\0'), opts]) 43 | 44 | 45 | def add_filter(self, tcm_info, attrs): 46 | handle = 0x0 47 | parent = self.qhandle 48 | tcm = tcmsg(socket.AF_UNSPEC, self.interface.if_index, handle, parent, tcm_info, 49 | attrs) 50 | 51 | msg = Message(type=RTM_NEWTFILTER, 52 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 53 | service_template=tcm) 54 | self.conn.send(msg) 55 | self.check_ack(self.conn.recv()) 56 | 57 | self.delete_root_qdisc() 58 | 59 | def add_htb_qdisc(self): 60 | tcm = tcmsg(socket.AF_UNSPEC, self.interface.if_index, self.qhandle, TC_H_ROOT, 0, 61 | [Attr(TCA_KIND, 'htb\0'), HTBQdiscAttr(defcls=0x1ff)]) 62 | 63 | msg = Message(type=RTM_NEWQDISC, 64 | flags=NLM_F_EXCL | NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK, 65 | service_template=tcm) 66 | 67 | self.conn.send(msg) 68 | self.conn.recv() 69 | 70 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | 2 | ShaPy can be used to easily create an emulated network. ShaPy consists from two 3 | main parts: ShaPy Framework and ShaPy Emulation. ShaPy Framework allows 4 | accessing traffic control capabilities of the Linux kernel using the Netlink 5 | interface. ShaPy Emulation builds on the framework and focuses on building 6 | emulated network. 7 | 8 | ## Installation 9 | Standard distutils installation is supported. Since ShaPy is also in PyPI you 10 | can use: 11 | 12 | pip install shapy 13 | 14 | or if you prefer development version from the repo: 15 | 16 | pip install -e git://github.com/praus/shapy.git#egg=shapy 17 | 18 | or download/clone and install the package manually: 19 | 20 | python setup.py install 21 | 22 | ## Examples 23 | 24 | ### ShaPy Emulation 25 | 26 | The following snippet gives you a virtual network of three IP addresses with 27 | exactly the declared speeds and delays. 28 | 29 | ```python 30 | from shapy.emulation.shaper import Shaper 31 | 32 | ps = {("127.0.0.2",) : {'upload': 1024, 'download': 1024, 'delay': 56}, 33 | ("127.0.0.3",) : {'upload': 256, 'download': 512, 'delay': 30}, 34 | ("127.0.0.4",) : {'upload': 256, 'download': 512, 'delay': 30}, 35 | } 36 | 37 | sh = Shaper() 38 | sh.set_shaping(ps) 39 | ``` 40 | 41 | ### ShaPy Framework 42 | The following snippet is an example of ShaPy Framework. It creates a HTB qdisc 43 | as a root qdisc on interface `lo`, creates a HTB class with maximum throughput 44 | 500 kbit (units are set in settings, see below) and a filter that will redirect 45 | all traffic with source IP 127.0.0.3 to the HTB class to be shaped. 46 | 47 | ```python 48 | lo = Interface('lo') 49 | h1 = HTBQdisc('1:', default_class='1ff') 50 | h1.add( FlowFilter('src 127.0.0.3', '1:1', prio=2) ) 51 | h1.add( HTBClass('1:1', rate=500, ceil=500) ) 52 | lo.add( h1 ) 53 | lo.set_shaping() 54 | ``` 55 | 56 | ### ShaPy Settings 57 | Shapy framework provides a simple facility for managing settings. User creates a 58 | regular Python file accessible as a module. In his main program he subsequently 59 | registers this module as shown below. The module name is not important but the 60 | convention is to call it `settings.py`. The custom settings file can override or 61 | add values specified in the default ShaPy settings module 62 | `shapy.framework.settings.default`. 63 | 64 | 65 | ```python 66 | import shapy 67 | shapy.register_settings('settings') 68 | ``` 69 | 70 | Settings file for ShaPy Emulation might look like this: 71 | 72 | ```python 73 | COMMANDS = 'shapy.emulation.commands' 74 | UNITS = 'kbit' 75 | 76 | ### CWC settings ### 77 | 78 | # it's advisable to set MTU of the interfaces to something real, for example 1500 79 | EMU_INTERFACES = ( 80 | 'lo', 81 | ) 82 | 83 | # ports excluded from shaping and delaying (holds for in and out) 84 | # usually used for control ports 85 | EMU_NOSHAPE_PORTS = (8000,) 86 | 87 | ``` -------------------------------------------------------------------------------- /shapy/framework/filter.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import re 3 | from shapy.framework.netlink import NetlinkExecutable 4 | from shapy.framework.netlink.constants import * 5 | from shapy.framework.netlink.message import * 6 | from shapy.framework.netlink.filter import * 7 | from shapy.framework.exceptions import ImproperlyConfigured 8 | from shapy.framework.utils import convert_handle, get_if_index, validate_full_ip 9 | 10 | class Filter(NetlinkExecutable): 11 | type = RTM_NEWTFILTER 12 | children = [] # filter cannot have children 13 | 14 | class U32Filter(Filter): 15 | """Abstract filter for matching IP address.""" 16 | def __init__(self, match, **kwargs): 17 | Filter.__init__(self, **kwargs) 18 | self.attrs = [Attr(TCA_KIND, 'u32\0')] 19 | 20 | dir, val = match.split() 21 | if validate_full_ip(val): 22 | ip_packed = socket.inet_aton(val) 23 | val = struct.unpack("I", ip_packed)[0] 24 | else: 25 | val = int(val) 26 | off_map = {'src': 12, 'dst': 16, 'sport': 20, 'dport': 22} 27 | # A word of warning: IP packet header can have variable length (up to 15 28 | # 32bit-words), therefore the above sport and dport offsets might be 29 | # wrong under very specific circumstances. This is wrong according to 30 | # RFC 791 (IPv4 specs) but tc tool does not solve this problem either. 31 | try: 32 | self.selector = u32_selector(val=val, 33 | offset=off_map[dir], 34 | mask=kwargs.pop('mask', 0xffffffff)) 35 | except KeyError: 36 | raise ImproperlyConfigured("Invalid direction, must be either src or dst") 37 | 38 | self.opts.update({'handle': 0x0}) 39 | 40 | class RedirectFilter(U32Filter): 41 | """Redirecting traffic to (mainly) IFB devices""" 42 | def __init__(self, match, ifb, **kwargs): 43 | U32Filter.__init__(self, match, **kwargs) 44 | prio = kwargs.get('prio', 2) 45 | 46 | protocol = ETH_P_IP 47 | self.tcm_info = prio << 16 | protocol 48 | 49 | if_index = get_if_index(ifb) 50 | flow = u32_classid(convert_handle(0x10001)) 51 | action = u32_mirred_action(if_index) 52 | self.attrs.append(Attr(TCA_OPTIONS, 53 | flow.pack()+action.pack()+self.selector.pack())) 54 | 55 | 56 | class FlowFilter(U32Filter): 57 | """Classifying traffic to classes.""" 58 | def __init__(self, match, flowid, **kwargs): 59 | U32Filter.__init__(self, match, **kwargs) 60 | prio = kwargs.get('prio', 3) 61 | protocol = ETH_P_IP 62 | self.tcm_info = prio << 16 | protocol 63 | 64 | flow = u32_classid(convert_handle(flowid)) 65 | self.attrs.append(Attr(TCA_OPTIONS, flow.pack() + self.selector.pack())) 66 | 67 | -------------------------------------------------------------------------------- /shapy/framework/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import re 3 | import struct 4 | from shapy.framework.exceptions import ImproperlyConfigured 5 | from shapy.framework.executor import run 6 | from shapy import settings 7 | 8 | with open('/proc/net/psched', 'rb') as f: 9 | psched = f.read() 10 | ns_per_usec, ns_per_tick = psched.strip().split()[:2] 11 | ns_per_usec = struct.unpack(">I", ns_per_usec.decode('hex'))[0] 12 | ns_per_tick = struct.unpack(">I", ns_per_tick.decode('hex'))[0] 13 | ticks_per_usec = float(ns_per_usec) / float(ns_per_tick) 14 | 15 | def validate_ip(addr): 16 | assert isinstance(addr, str), "IP address must be a string" 17 | try: 18 | socket.inet_aton(addr) 19 | except socket.error: 20 | raise ImproperlyConfigured("Invalid IP: %s" % addr) 21 | 22 | # http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address 23 | valid_IP_pattern = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") 24 | def validate_full_ip(ip): 25 | return bool(re.match(valid_IP_pattern, ip)) 26 | 27 | def align(l, alignto=4): 28 | """Aligned length to nearest multiple of 4.""" 29 | return (l + alignto - 1) & ~(alignto - 1) 30 | 31 | def convert_handle(handle): 32 | """ 33 | Takes string handle such as 1: or 10:1 and creates a binary number accepted 34 | by the kernel Traffic Control. 35 | """ 36 | if isinstance(handle, str): 37 | major, minor = handle.split(':') # "major:minor" 38 | minor = minor if minor else '0' 39 | return int(major, 16) << 16 | int(minor, 16) 40 | return handle 41 | 42 | def get_if_index(if_name): 43 | """ 44 | Retrieves interface index based on interface name. 45 | Ugly implementation of if_nametoindex() from net/if.h 46 | """ 47 | out = run("ip link show dev {interface}".format(interface=if_name)) 48 | try: 49 | return int(re.match('^([0-9]+)', out).group(0), 10) 50 | except: 51 | return 0 52 | 53 | def nl_us2ticks(delay): 54 | """Convert microseconds to timer ticks.""" 55 | return int(ticks_per_usec * delay) 56 | 57 | def nl_ticks2us(ticks): 58 | """Convert ticks to microseconds.""" 59 | return ticks / ticks_per_usec 60 | 61 | def convert_to_bytes(rate): 62 | """ 63 | Converts rate in kilobits or kilobytes to bytes based on shapy.settings 64 | """ 65 | r = rate * 1000 66 | if settings.UNITS == "kbit": 67 | r = r / 8 68 | return r 69 | 70 | class InterpreterMixin(object): 71 | interpreters = {} 72 | 73 | @classmethod 74 | def register(cls, interpreter, content_types): 75 | for type in content_types: 76 | cls.interpreters.update({type: interpreter}) 77 | 78 | @classmethod 79 | def select(cls, selector): 80 | """Selects interpreter object based on the selector.""" 81 | return cls.interpreters[selector] 82 | -------------------------------------------------------------------------------- /shapy/framework/executor.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import re 3 | import subprocess 4 | import shlex 5 | import logging 6 | import logging.handlers 7 | #from string import Template 8 | 9 | logger = logging.getLogger('shapy.framework.executor') 10 | 11 | from shapy import settings 12 | from shapy.framework.exceptions import ImproperlyConfigured 13 | 14 | def run(command, **kwargs): 15 | command = shlex.split(command) 16 | if kwargs.pop('sudo', True): 17 | if settings.SUDO_PASSWORD: 18 | command.insert(0, '-S') 19 | command.insert(0, 'sudo') 20 | 21 | p = subprocess.Popen(command, bufsize=-1, stdout=subprocess.PIPE, 22 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 23 | env=settings.ENV) 24 | stdout, stderr = p.communicate('%s\n' % settings.SUDO_PASSWORD) 25 | 26 | if p.returncode == 0: 27 | logger.info('[{1}] {0}'.format(' '.join(command), p.returncode)) 28 | else: 29 | fmt = """[{1}] {0} [{2}]""" 30 | logger.error(fmt.format(' '.join(command), p.returncode, stderr.strip())) 31 | 32 | return stdout 33 | 34 | 35 | def get_command(name, **kwargs): 36 | try: 37 | __import__(settings.COMMANDS) 38 | cmd = getattr(sys.modules[settings.COMMANDS], name) 39 | if kwargs: 40 | cmd = cmd.format(**kwargs) 41 | return cmd 42 | except AttributeError: 43 | msg = "Command '%s' undefined!" % name 44 | logger.critical(msg) 45 | raise ImproperlyConfigured(msg) 46 | except KeyError, ImportError: 47 | msg = "Missing commands module (%s)!" % settings.COMMANDS 48 | logger.critical(msg) 49 | raise ImproperlyConfigured(msg) 50 | 51 | 52 | class Executable(object): 53 | def __init__(self, **kwargs): 54 | self.opts = kwargs 55 | self.executed = False 56 | 57 | def __setitem__(self, key, item): 58 | self.opts.update({key: item}) 59 | 60 | def __getitem__(self, key): 61 | return self.opts[key] 62 | 63 | @property 64 | def cmd(self): 65 | return get_command(self.__class__.__name__) 66 | 67 | def get(self): 68 | self.opts.update(self.get_context()) 69 | return self.cmd.format(**self.opts) 70 | 71 | def get_context(self): 72 | has_p = getattr(self, 'parent', None) 73 | return {'parent': self.parent['handle'] if has_p else '', 74 | 'interface': self.get_interface()} 75 | 76 | def get_interface(self): 77 | p = getattr(self, 'parent', self) 78 | while hasattr(p, 'parent'): 79 | p = getattr(p, 'parent') 80 | try: 81 | return getattr(p, 'interface') 82 | except AttributeError: 83 | msg = "Element {0!r} has no interface".format(self) 84 | logger.critical(msg) 85 | raise ImproperlyConfigured(msg) 86 | 87 | def execute(self): 88 | if not self.executed: 89 | run(self.get()) 90 | else: 91 | logger.debug("Command %s was already executed."% self.get()) 92 | -------------------------------------------------------------------------------- /shapy/framework/interface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger('shapy.framework.interface') 3 | 4 | from shapy.framework import executor, utils 5 | 6 | class Interface(object): 7 | interfaces = {} 8 | 9 | def __new__(cls, name): 10 | if Interface.interfaces.has_key(name): 11 | return Interface.interfaces[name] 12 | instance = super(Interface, cls).__new__(cls) 13 | Interface.interfaces.update({name: instance}) 14 | return instance 15 | 16 | def __del__(self): 17 | del Interface.interfaces[self.name] 18 | 19 | def __init__(self, name): 20 | self.name = name 21 | self.if_index = utils.get_if_index(self.name) 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | def add(self, root_qdisc): 27 | self.root = root_qdisc 28 | self.root.interface = self 29 | return self.root 30 | 31 | def add_ingress(self, ingress_qdisc): 32 | self.ingress = ingress_qdisc 33 | self.ingress.interface = self 34 | return self.ingress 35 | 36 | def set_shaping(self): 37 | #assert self.root, "Interface must contain at least a root qdisc." 38 | if hasattr(self, 'ingress'): 39 | self.ingress.execute() 40 | for ch in self.ingress.children: 41 | ch.execute() 42 | if hasattr(self, 'root'): 43 | self.__traverse_tree(self.root) 44 | 45 | def teardown(self): 46 | """ 47 | Tears down all custom qdiscs, classes and filters on this interface. 48 | """ 49 | cmd_e = executor.get_command('TCInterfaceTeardown', interface=self.name, 50 | handle='root') 51 | cmd_i = executor.get_command('TCInterfaceTeardown', interface=self.name, 52 | handle='ingress') 53 | executor.run(cmd_e) 54 | executor.run(cmd_i) 55 | 56 | def __traverse_tree(self, element): 57 | logger.debug("Interface '{0!s}' executing {1}"\ 58 | .format(self, element.__class__.__name__)) 59 | element.execute() 60 | for ch in element.children: 61 | self.__traverse_tree(ch) 62 | 63 | 64 | class IFB(Interface, executor.Executable): 65 | module_loaded = False 66 | 67 | def __init__(self, name): 68 | executor.Executable.__init__(self) 69 | self.name = name 70 | #Interface.__init__(self, name) 71 | 72 | def set_shaping(self): 73 | self.if_index = utils.get_if_index(self.name) 74 | self.execute() 75 | Interface.set_shaping(self) 76 | 77 | def get_context(self): 78 | return {'interface': self.name} 79 | 80 | @staticmethod 81 | def teardown(): 82 | """ 83 | A custom teardown, all we need to do is unload the module 84 | """ 85 | #Interface.teardown(self) 86 | if IFB.module_loaded: 87 | executor.run('rmmod ifb') 88 | IFB.module_loaded = False 89 | 90 | @staticmethod 91 | def modprobe(): 92 | if not IFB.module_loaded: 93 | executor.run('modprobe ifb') 94 | IFB.module_loaded = True 95 | -------------------------------------------------------------------------------- /shapy/framework/netlink/filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Please see doc/tc_filter_attr_structure.pdf for figure explaining attribute 3 | structure of TC filter messages. 4 | """ 5 | 6 | from shapy.framework.netlink.message import Attr 7 | from .constants import * 8 | from struct import Struct 9 | 10 | class u32_classid(Attr): 11 | data_format = Struct("I") 12 | 13 | def __init__(self, classid): 14 | data = self.data_format.pack(classid) 15 | super(u32_classid, self).__init__(TCA_U32_CLASSID, data) 16 | 17 | 18 | class u32_selector(Attr): 19 | #struct tc_u32_sel { 20 | # unsigned char flags; 21 | # unsigned char offshift; 22 | # unsigned char nkeys; 23 | # 24 | # __be16 offmask; 25 | # __u16 off; 26 | # short offoff; 27 | # 28 | # short hoff; 29 | # __be32 hmask; 30 | # struct tc_u32_key keys[0]; 31 | #}; 32 | 33 | #struct tc_u32_key { 34 | # __be32 mask; 35 | # __be32 val; 36 | # int off; 37 | # int offmask; 38 | #}; 39 | 40 | tc_u32_sel = Struct("BBBHHhhI") 41 | tc_u32_key = Struct("IIii") 42 | data_format = Struct(tc_u32_sel.format+tc_u32_key.format) 43 | 44 | def __init__(self, val, offset, mask=0xffffffff, offmask=0): 45 | """ 46 | Selector for u32 filter, value is IP address for example. 47 | offset is 12 (0xc) for source IP, 16 for destination IP. 48 | 49 | This is rather crude, underlying kernel infrastructure supports multiple 50 | keys, but since there was no need for such a functionality, it is left 51 | out. See iproute2 sources for example how to implement it. 52 | """ 53 | flags = TC_U32_TERMINAL 54 | nkeys = 1 55 | sel = self.tc_u32_sel.pack(flags, 0, nkeys, 0, 0, 0, 0, 0) 56 | key = self.tc_u32_key.pack(mask, val, offset, offmask) 57 | data = sel+key 58 | super(u32_selector, self).__init__(TCA_U32_SEL, data) 59 | 60 | 61 | class u32_mirred_action(Attr): 62 | """ 63 | The basic action redirecting all matched traffic to the interface specified 64 | by ifindex using mirred strategy. 65 | """ 66 | # include/linux/tc_act/tc_mirred.h 67 | ##define tc_gen \ 68 | # __u32 index; \ 69 | # __u32 capab; \ 70 | # int action; \ 71 | # int refcnt; \ 72 | # int bindcnt 73 | #struct tc_mirred { 74 | # tc_gen; 75 | # int eaction; /* one of IN/EGRESS_MIRROR/REDIR */ 76 | # __u32 ifindex; /* ifindex of egress port */ 77 | #}; 78 | 79 | data_format = Struct("IIiiiiI") 80 | 81 | def __init__(self, ifindex): 82 | tc_mirred_parms = Attr(TCA_MIRRED_PARMS, 83 | self.data_format.pack(0, 0, TCA_INGRESS_MIRROR, 0, 0, 84 | TCA_EGRESS_REDIR, ifindex)) 85 | kind = Attr(TCA_ACT_KIND, "mirred\0") 86 | options = Attr(TCA_ACT_OPTIONS, tc_mirred_parms.pack()) 87 | redir = Attr(TCA_EGRESS_REDIR, kind.pack()+options.pack()) 88 | super(u32_mirred_action, self).__init__(TCA_U32_ACT, redir.pack()) 89 | -------------------------------------------------------------------------------- /tests/emulation/test_shaping.py: -------------------------------------------------------------------------------- 1 | #import logging 2 | #logging.basicConfig(level=logging.INFO, datefmt='%H:%M:%S', 3 | # format='%(asctime)s %(levelname)s: %(message)s') 4 | 5 | import unittest 6 | import SocketServer, socket 7 | import random, time 8 | import threading 9 | import cStringIO 10 | from datetime import datetime 11 | 12 | from shapy import register_settings 13 | register_settings('tests.emulation.settings') 14 | from shapy.emulation.shaper import Shaper 15 | from tests.mixins import ShaperMixin, ServerMixin 16 | from tests.utils import total_seconds 17 | 18 | class TestCWCShaping(unittest.TestCase, ShaperMixin, ServerMixin): 19 | filesize = 2**19 # 0.5MB 20 | 21 | def setUp(self): 22 | self.server_addr = ('127.0.0.2', 55000) 23 | self.client_addr = ('127.0.0.3', 55001) 24 | 25 | # shaping init 26 | ShaperMixin.setUp(self) 27 | 28 | ServerMixin.run_server(self) 29 | 30 | with open('/dev/urandom', 'rb') as f: 31 | self.randomfile = bytearray(f.read(self.filesize)) 32 | 33 | 34 | def test_transfer(self): 35 | self.sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 36 | # SO_REUSEADDR: http://stackoverflow.com/questions/3229860/what-is-the-meaning-of-so-reuseaddr-setsockopt-option-linux 37 | s = self.sock_client 38 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 39 | s.bind(self.client_addr) 40 | s.connect(self.server_addr) 41 | start = datetime.now() 42 | 43 | # client -> server 44 | sent = 0 45 | while sent < self.filesize: 46 | sent += s.send(self.randomfile[sent:sent+4096]) 47 | 48 | # We have to wait until the server finishes reading data from its socket 49 | # and closes the connection. 50 | rcvd = s.recv(1024) 51 | 52 | delay = total_seconds(datetime.now() - start) 53 | #delay = delta.seconds + delta.microseconds/float(10**6) 54 | tt = self.estimate_transfer_time(self.filesize, self.client_addr[0], 55 | self.server_addr[0]) 56 | self.assertAlmostEqual(delay, tt, delta=0.4) 57 | 58 | # server -> client 59 | start = datetime.now() 60 | 61 | while len(rcvd) < self.filesize: 62 | rcvd += s.recv(1024) 63 | 64 | delay = total_seconds(datetime.now() - start) 65 | tt = self.estimate_transfer_time(self.filesize, self.server_addr[0], 66 | self.client_addr[0]) 67 | self.assertAlmostEqual(delay, tt, delta=0.4) 68 | 69 | # statistics of qdiscs on IFB must correctly reflect the transmitted data 70 | self._test_traffic() 71 | 72 | s.close() 73 | 74 | def _test_traffic(self): 75 | c = self.sh.get_traffic(self.client_addr[0]) 76 | s = self.sh.get_traffic(self.server_addr[0]) 77 | # qdisc statistics reflect all traffic, including header of each layer, 78 | # not only filesize 79 | delta = self.filesize/100 80 | self.assertAlmostEqual(c[0], self.filesize, delta=delta) 81 | self.assertAlmostEqual(c[1], self.filesize, delta=delta) 82 | self.assertAlmostEqual(s[0], self.filesize, delta=delta) 83 | self.assertAlmostEqual(s[1], self.filesize, delta=delta) 84 | 85 | 86 | def tearDown(self): 87 | if hasattr(self, 'sock_client'): 88 | self.sock_client.close() 89 | ShaperMixin.tearDown(self) 90 | -------------------------------------------------------------------------------- /shapy/framework/netlink/constants.py: -------------------------------------------------------------------------------- 1 | TC_H_UNSPEC = 0 2 | TC_H_ROOT = 0xFFFFFFFF 3 | TC_H_INGRESS = 0xFFFFFFF1 4 | 5 | # flags 6 | NLM_F_REQUEST = 1 7 | NLM_F_MULTI = 2 8 | NLM_F_ACK = 4 9 | NLM_F_ECHO = 8 10 | 11 | # Modifiers (flags) to GET requests 12 | NLM_F_ROOT = 0x100 13 | NLM_F_MATCH = 0x200 14 | NLM_F_ATOMIC = 0x400 15 | NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH 16 | 17 | # Modifiers to NEW request 18 | NLM_F_REPLACE = 0x100 # Override existing 19 | NLM_F_EXCL = 0x200 # Do not touch, if it exists 20 | NLM_F_CREATE = 0x400 # Create, if it does not exist 21 | NLM_F_APPEND = 0x800 # Add to end of list 22 | 23 | 24 | ################### 25 | ## Message types ## 26 | ################### 27 | 28 | NLMSG_NOOP = 1 29 | NLMSG_ERROR = 2 30 | NLMSG_DONE = 3 31 | NLMSG_OVERRUN = 4 32 | NLMSG_MIN_TYPE = 0x10 33 | 34 | RTM_BASE = 16 35 | 36 | RTM_NEWLINK = 16 37 | RTM_DELLINK = 17 38 | RTM_GETLINK = 18 39 | RTM_SETLINK = 19 40 | 41 | RTM_NEWADDR = 20 42 | RTM_DELADDR = 21 43 | RTM_GETADDR = 22 44 | 45 | RTM_NEWROUTE = 24 46 | RTM_DELROUTE = 25 47 | RTM_GETROUTE = 26 48 | 49 | RTM_NEWNEIGH = 28 50 | RTM_DELNEIGH = 29 51 | RTM_GETNEIGH = 30 52 | 53 | RTM_NEWRULE = 32 54 | RTM_DELRULE = 33 55 | RTM_GETRULE = 34 56 | 57 | RTM_NEWQDISC = 36 58 | RTM_DELQDISC = 37 59 | RTM_GETQDISC = 38 60 | 61 | RTM_NEWTCLASS = 40 62 | RTM_DELTCLASS = 41 63 | RTM_GETTCLASS = 42 64 | 65 | RTM_NEWTFILTER = 44 66 | RTM_DELTFILTER = 45 67 | RTM_GETTFILTER = 46 68 | 69 | RT_TABLE_MAIN = 254 70 | 71 | 72 | IFLA_MTU = 4 73 | 74 | ### Traffic Control (Qdisc) ### 75 | TCA_UNSPEC = 0 76 | TCA_KIND = 1 77 | TCA_OPTIONS = 2 78 | TCA_STATS = 3 79 | TCA_XSTATS = 4 80 | TCA_RATE = 5 81 | TCA_FCNT = 6 82 | TCA_STATS2 = 7 83 | TCA_STAB = 8 84 | 85 | 86 | ### HTB Class ### 87 | TCA_HTB_UNSPEC = 0 88 | TCA_HTB_PARMS = 1 89 | TCA_HTB_INIT = 2 90 | TCA_HTB_CTAB = 3 91 | TCA_HTB_RTAB = 4 92 | 93 | 94 | ### Filter attributes ### 95 | TCA_U32_UNSPEC = 0 96 | TCA_U32_CLASSID = 1 97 | TCA_U32_HASH = 2 98 | TCA_U32_LINK = 3 99 | TCA_U32_DIVISOR = 4 100 | TCA_U32_SEL = 5 101 | TCA_U32_POLICE = 6 102 | TCA_U32_ACT = 7 103 | TCA_U32_INDEV = 8 104 | TCA_U32_PCNT = 9 105 | TCA_U32_MARK = 10 106 | 107 | # U32 filter flags 108 | TC_U32_TERMINAL = 1 109 | TC_U32_OFFSET = 2 110 | TC_U32_VAROFFSET = 4 111 | TC_U32_EAT = 8 112 | TC_U32_MAXDEPTH = 8 113 | 114 | # Filter actions 115 | TCA_ACT_MIRRED = 8 116 | TCA_EGRESS_REDIR = 1 # packet redirect to EGRESS 117 | TCA_EGRESS_MIRROR = 2 # mirror packet to EGRESS 118 | TCA_INGRESS_REDIR = 3 # packet redirect to INGRESS 119 | TCA_INGRESS_MIRROR = 4 # mirror packet to INGRESS 120 | 121 | TCA_ACT_UNSPEC = 0 122 | TCA_ACT_KIND = 1 123 | TCA_ACT_OPTIONS = 2 124 | TCA_ACT_INDEX = 3 125 | TCA_ACT_STATS = 4 126 | 127 | # filter action flags 128 | TC_ACT_UNSPEC = -1 129 | TC_ACT_OK = 0 130 | TC_ACT_RECLASSIFY = 1 131 | TC_ACT_SHOT = 2 132 | TC_ACT_PIPE = 3 133 | TC_ACT_STOLEN = 4 134 | TC_ACT_QUEUED = 5 135 | TC_ACT_REPEAT = 6 136 | TC_ACT_JUMP = 0x10000000 137 | 138 | # mirred params 139 | TCA_MIRRED_UNSPEC = 0 140 | TCA_MIRRED_TM = 1 141 | TCA_MIRRED_PARMS = 2 142 | 143 | 144 | 145 | ### Kernel Ethernet protocol numbers 146 | # see include/linux/if_ether.h 147 | ETH_P_IP = 0x08 148 | 149 | #enum { 150 | # RTM_NEWACTION = 48, 151 | # RTM_DELACTION, 152 | # RTM_GETACTION, 153 | 154 | # RTM_NEWPREFIX = 52, 155 | 156 | # RTM_GETMULTICAST = 58, 157 | 158 | # RTM_GETANYCAST = 62, 159 | 160 | # RTM_NEWNEIGHTBL = 64, 161 | # RTM_GETNEIGHTBL = 66, 162 | # RTM_SETNEIGHTBL, 163 | 164 | # RTM_NEWNDUSEROPT = 68, 165 | 166 | # RTM_NEWADDRLABEL = 72, 167 | # RTM_DELADDRLABEL, 168 | # RTM_GETADDRLABEL, 169 | 170 | # RTM_GETDCB = 78, 171 | # RTM_SETDCB, 172 | #}; -------------------------------------------------------------------------------- /shapy/framework/netlink/htb.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shapy.framework.netlink.message import Attr 3 | from .constants import * 4 | from struct import Struct 5 | from shapy.framework.utils import nl_us2ticks 6 | 7 | class HTBQdiscAttr(Attr): 8 | """Representation of HTB qdisc options.""" 9 | #struct tc_htb_glob { 10 | # __u32 version; /* to match HTB/TC */ 11 | # __u32 rate2quantum; /* bps->quantum divisor */ 12 | # __u32 defcls; /* default class number */ 13 | # __u32 debug; /* debug flags */ 14 | # 15 | # /* stats */ 16 | # __u32 direct_pkts; /* count of non shapped packets */ 17 | #}; 18 | 19 | data_format = Struct('6I') 20 | 21 | def __init__(self, defcls, r2q=10): 22 | data = self.data_format.pack(0x20018, 3, r2q, defcls, 0, 0) 23 | Attr.__init__(self, TCA_OPTIONS, data) 24 | 25 | 26 | class HTBParms(Attr): 27 | """ 28 | Internal units: bytes 29 | """ 30 | #struct tc_ratespec { 31 | # unsigned char cell_log; 32 | # unsigned char __reserved; 33 | # unsigned short overhead; 34 | # short cell_align; 35 | # unsigned short mpu; 36 | # __u32 rate; 37 | #}; 38 | #struct tc_htb_opt { 39 | # struct tc_ratespec rate; 40 | # struct tc_ratespec ceil; 41 | # __u32 buffer; 42 | # __u32 cbuffer; 43 | # __u32 quantum; 44 | # __u32 level; /* out only */ 45 | # __u32 prio; 46 | #}; 47 | 48 | tc_ratespec = Struct("BxHhHI") 49 | tc_htb_opt = Struct("5I") 50 | data_format = Struct("8xI8xI5I") # tc_htb_opt 51 | 52 | @classmethod 53 | def unpack(cls, data): 54 | attr, rest = Attr.unpack(data) 55 | d = cls.data_format.unpack(attr.data) 56 | return cls(d[0], d[1], d[5], d[7]) 57 | 58 | def __init__(self, rate, ceil=0, mtu=1600, quantum=0, prio=0): 59 | """ 60 | rate, ceil, mtu: bytes 61 | """ 62 | if not ceil: ceil = rate 63 | r = self.tc_ratespec.pack(3, 0, -1, 0, rate) 64 | c = self.tc_ratespec.pack(3, 0, -1, 0, ceil) 65 | hz = os.sysconf('SC_CLK_TCK') 66 | buffer = tc_calc_xmittime(rate, (rate / hz) + mtu) 67 | cbuffer = tc_calc_xmittime(ceil, (rate / hz) + mtu) 68 | t = self.tc_htb_opt.pack(buffer, cbuffer, quantum, 0, prio) 69 | data = r + c + t 70 | Attr.__init__(self, TCA_HTB_PARMS, data) 71 | 72 | 73 | class RTab(Attr): 74 | """ 75 | Rate table attribute, 256 integers representing an estimate how long it 76 | takes to send packets of various lengths. 77 | """ 78 | data_format = Struct("256I") 79 | 80 | def __init__(self, rate, mtu, cell_log=3): 81 | rtab = tc_calc_rtable(rate, cell_log, mtu) 82 | data = self.data_format.pack(*rtab) 83 | Attr.__init__(self, TCA_HTB_RTAB, data) 84 | 85 | class CTab(RTab): 86 | def __init__(self, rate, mtu, cell_log=3): 87 | rtab = tc_calc_rtable(rate, cell_log, mtu) 88 | data = self.data_format.pack(*rtab) 89 | Attr.__init__(self, TCA_HTB_CTAB, data) 90 | 91 | 92 | def tc_calc_rtable(rate, cell_log, mtu): 93 | """ 94 | rtab[pkt_len>>cell_log] = pkt_xmit_time 95 | 96 | cell - The cell size determines he granularity of packet transmission time 97 | calculations. Has a sensible default. 98 | 99 | """ 100 | # http://kerneltrap.org/mailarchive/linux-netdev/2009/11/2/6259456/thread 101 | rtab = [] 102 | bps = rate 103 | 104 | if mtu == 0: 105 | mtu = 2047 106 | 107 | if cell_log < 0: 108 | cell_log = 0 109 | while (mtu >> cell_log) > 255: 110 | cell_log += 1 111 | 112 | for i in range(0, 256): 113 | size = (i + 1) << cell_log 114 | rtab.append(tc_calc_xmittime(bps, size)) 115 | 116 | return rtab; 117 | 118 | def tc_calc_xmittime(rate, size): 119 | TIME_UNITS_PER_SEC = 1000000#000 120 | return int(nl_us2ticks(int(TIME_UNITS_PER_SEC*(float(size)/rate)))) 121 | 122 | -------------------------------------------------------------------------------- /tests/mixins.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import socket 3 | from datetime import datetime 4 | import time 5 | 6 | from shapy.emulation.shaper import Shaper 7 | from shapy.framework.netlink.message import * 8 | from shapy.framework.netlink.tc import * 9 | from shapy.framework.netlink.constants import * 10 | from tests.utils import total_seconds, eta 11 | 12 | 13 | class ShaperMixin(object): 14 | def setUp(self): 15 | self.shaper_conf = { 16 | (self.server_addr[0],) : {'upload': 256, 'download': 1024, 'delay': 5}, 17 | (self.client_addr[0],) : {'upload': 128, 'download': 512, 'delay': 30}, 18 | } 19 | self.sh = Shaper() 20 | self.sh.set_shaping(self.shaper_conf) 21 | 22 | def tearDown(self): 23 | self.sh.teardown_all() 24 | 25 | def estimate_transfer_time(self, filesize, a, b): 26 | """ 27 | Determines how long the transfer of filesize bytes from A to B should take. 28 | units for filesize are bytes, a and b are IP addresses 29 | """ 30 | up = self.shaper_conf[(a,)]['upload'] 31 | down = self.shaper_conf[(b,)]['download'] 32 | return eta(filesize, up, down) 33 | 34 | def estimate_delay(self, a, b): 35 | da = self.shaper_conf[(a,)]['delay'] 36 | db = self.shaper_conf[(b,)]['delay'] 37 | return da+db 38 | 39 | 40 | class ServerMixin(object): 41 | """ 42 | Expected members of a class this object is mixed into: 43 | filesize: size of transferred file in bytes 44 | server_addr: (address,port) the server should bind to 45 | """ 46 | class Server(threading.Thread): 47 | def __init__(self, server, filesize): 48 | threading.Thread.__init__(self) 49 | self.server_addr = server 50 | self.filesize = filesize 51 | 52 | def run(self): 53 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 54 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 55 | s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 50688) # 49.5 KB 56 | s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 87380) # 85.3 KB 57 | s.bind(self.server_addr) 58 | s.listen(1) 59 | conn, addr = s.accept() 60 | data = bytearray() 61 | while len(data) < self.filesize: 62 | data += conn.recv(1024) 63 | 64 | sent = 0 65 | while sent < self.filesize: 66 | sent += conn.send(data[sent:sent+4096]) 67 | 68 | conn.recv(1) 69 | conn.close() 70 | s.close() 71 | 72 | def run_server(self): 73 | self.server_thread = ServerMixin.Server(self.server_addr, self.filesize) 74 | self.server_thread.daemon = True 75 | self.server_thread.start() 76 | time.sleep(0.1) # wait for server to initialize 77 | 78 | 79 | class ClientMixin(object): 80 | """ 81 | Expected members of a class this object is mixed into: 82 | randomfile: the array to be transmitted over the "virtual network" 83 | filesize: size of transferred file in bytes 84 | server_addr: (address,port) of a server the client will connect to 85 | client_addr: (address,port) the client will bind to 86 | """ 87 | 88 | def make_transfer(self): 89 | self.sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 90 | # SO_REUSEADDR: http://stackoverflow.com/questions/3229860/what-is-the-meaning-of-so-reuseaddr-setsockopt-option-linux 91 | s = self.sock_client 92 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 93 | s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 50688) # 49.5 KB 94 | s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 87380) # 85.3 KB 95 | s.bind(self.client_addr) 96 | s.connect(self.server_addr) 97 | start = datetime.now() 98 | 99 | ## client -> server 100 | sent = 0 101 | while sent < self.filesize: 102 | sent += s.send(self.randomfile[sent:sent+4096]) 103 | 104 | # We have to wait until the server finishes reading data from its socket 105 | # and closes the connection. 106 | rcvd = s.recv(1) 107 | time_up = total_seconds(datetime.now() - start) 108 | 109 | time.sleep(getattr(self, 'sleep', 0)) # wait for a bucket to fill again 110 | 111 | ## server -> client 112 | start = datetime.now() 113 | 114 | while len(rcvd) < self.filesize: 115 | rcvd += s.recv(1024) 116 | 117 | time_down = total_seconds(datetime.now() - start) 118 | s.close() 119 | return time_up, time_down 120 | -------------------------------------------------------------------------------- /shapy/framework/netlink/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains code pertaining to the generic messaging on a Netlink socket. 3 | """ 4 | 5 | #There are three levels to a Netlink message: The general Netlink 6 | #message header, the IP service specific template, and the IP service 7 | #specific data. 8 | 9 | # 0 1 2 3 10 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 11 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 12 | #| | 13 | #| Netlink message header | 14 | #| | 15 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 16 | #| | 17 | #| IP Service Template | 18 | #| | 19 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | #| | 21 | #| IP Service specific data in TLVs | 22 | #| | 23 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | 25 | import quopri, binascii 26 | import struct 27 | from struct import Struct 28 | from shapy.framework.utils import align, InterpreterMixin 29 | from .constants import * 30 | 31 | class Message(object): 32 | """Object representing the entire Netlink message.""" 33 | #0 1 2 3 34 | #0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 35 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 36 | #| Length | 37 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 38 | #| Type | Flags | 39 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | #| Sequence Number | 41 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 42 | #| Process ID (PID) | 43 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 44 | 45 | #struct nlmsghdr { 46 | # __u32 nlmsg_len; /* Length of message including header. */ 47 | # __u16 nlmsg_type; /* Type of message content. */ 48 | # __u16 nlmsg_flags; /* Additional flags. */ 49 | # __u32 nlmsg_seq; /* Sequence number. */ 50 | # __u32 nlmsg_pid; /* PID of the sending process. */ 51 | #}; 52 | 53 | nlmsghdr = Struct("IHHII") 54 | 55 | @classmethod 56 | def unpack(cls, msg): 57 | """Unpack raw bytes into a Netlink message.""" 58 | mlength, type, flags, seq, pid = cls.nlmsghdr.unpack(msg[:cls.nlmsghdr.size]) 59 | st = None 60 | if msg[cls.nlmsghdr.size:] > 0: 61 | st_cls = ServiceTemplate.select(type) 62 | st = st_cls.unpack(msg[cls.nlmsghdr.size:]) 63 | return Message(type, flags, seq, service_template=st) 64 | 65 | def __init__(self, type, flags=0, seq=-1, service_template=None): 66 | """Used for creating Netlink messages.""" 67 | self.type = type 68 | self.flags = flags 69 | self.seq = seq 70 | self.pid = 0 71 | self.service_template = service_template 72 | self.service_template.message = self 73 | 74 | def __len__(self): 75 | """Aligned length of service template message + attributes.""" 76 | return align(self.nlmsghdr.size + len(self.payload)) 77 | 78 | def pack_header(self): 79 | return self.nlmsghdr.pack(len(self), self.type, self.flags, self.seq, self.pid) 80 | 81 | def pack(self): 82 | return self.pack_header() + self.payload 83 | 84 | @property 85 | def payload(self): 86 | return self.service_template.pack() 87 | 88 | def __repr__(self): 89 | return '' % ( 90 | self.type, self.pid, self.seq, self.flags) 91 | 92 | 93 | class ServiceTemplate(InterpreterMixin): 94 | """Represents the second part of the Netlink message.""" 95 | @classmethod 96 | def unpack(cls, data): 97 | rest = data[cls.format.size:] 98 | st_instance = cls(*cls.format.unpack(data[:cls.format.size])) 99 | st_instance.attributes = tuple(st_instance.unpack_attrs(rest)) 100 | return st_instance 101 | 102 | def pack(self): 103 | return '' 104 | 105 | def pack_attrs(self): 106 | return ''.join(( a.pack() for a in self.attributes )) 107 | 108 | def unpack_attrs(self, data): 109 | while True: 110 | attr, data = Attr.unpack(data) 111 | attr.service_template = self 112 | yield attr 113 | if not data: break 114 | 115 | 116 | class ACK(ServiceTemplate): 117 | """ 118 | Netlink ACK message representation (RFC 3549, p. 12). 119 | """ 120 | # 0 1 2 3 121 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 122 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 123 | #| Netlink message header | 124 | #| type = NLMSG_ERROR | 125 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 126 | #| Error code | 127 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 128 | #| OLD Netlink message header | 129 | #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 130 | 131 | format = Struct('i') 132 | 133 | @classmethod 134 | def unpack(cls, data): 135 | rest = data[cls.format.size:] 136 | st_instance = cls(*cls.format.unpack(data[:cls.format.size])) 137 | st_instance.attributes = [] 138 | st_instance.old_message = rest.encode('string_escape') 139 | return st_instance 140 | 141 | def __init__(self, error_code): 142 | self.error_code = error_code 143 | 144 | def pack(self): 145 | self.format.pack(self.error_code) 146 | 147 | def is_error(self): 148 | return bool(self.error_code) 149 | 150 | ServiceTemplate.register(ACK, (NLMSG_ERROR, NLMSG_DONE)) 151 | 152 | class Attr(object): 153 | """Represents a single attribute.""" 154 | #struct rtattr { 155 | # unsigned short rta_len; /* Length of option */ 156 | # unsigned short rta_type; /* Type of option */ 157 | # /* Data follows */ 158 | #}; 159 | 160 | rtattr = Struct('HH') 161 | 162 | @classmethod 163 | def unpack(cls, data): 164 | length, type = cls.rtattr.unpack(data[:cls.rtattr.size]) 165 | data_len = length-cls.rtattr.size 166 | attr_data = struct.unpack('{0}s'.format(data_len), 167 | data[cls.rtattr.size:cls.rtattr.size+data_len])[0] 168 | return cls(type, attr_data), data[align(length):] 169 | 170 | def __init__(self, rta_type, payload): 171 | self.rta_type = rta_type 172 | self.payload = payload 173 | self.data = self.unpack_data() 174 | 175 | def pack(self): 176 | data = struct.pack('{0}s'.format(len(self.payload)), self.payload) 177 | rta_len = self.rtattr.size+len(data) 178 | return struct.pack('{0}{1}s'.format(self.rtattr.format, align(len(data))), 179 | rta_len, self.rta_type, data) 180 | 181 | def unpack_data(self): 182 | if hasattr(self, 'data_format') and hasattr(self, 'data_struct'): 183 | return self.data_struct._make(self.data_format.unpack(self.payload)) 184 | return None 185 | 186 | def __repr__(self): 187 | return """""" % ( 188 | align(len(self.payload)), self.rta_type, 189 | self.payload.encode('string_escape')) 190 | 191 | def unpack_attrs(data): 192 | while True: 193 | attr, data = Attr.unpack(data) 194 | yield attr 195 | if not data: break 196 | 197 | -------------------------------------------------------------------------------- /tests/netlink/test_unpack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import socket 4 | import struct 5 | from struct import Struct 6 | 7 | from shapy.framework.netlink.constants import * 8 | from shapy.framework.netlink.message import * 9 | from shapy.framework.netlink.tc import * 10 | from shapy.framework.netlink.connection import Connection 11 | from shapy.framework.netlink.netem import * 12 | from shapy.framework.netlink.filter import * 13 | from shapy.framework.utils import nl_us2ticks 14 | 15 | from tests.utils import * 16 | 17 | class TestUnpack(unittest.TestCase): 18 | def setUp(self): 19 | self.conn = Connection() 20 | 21 | #def test_unpack_add_qdisc(self): 22 | # aq = "\x48\x00\x00\x00\x24\x00\x05\x06\x4d\x34\xc1\x4d\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\xff\xff\xff\xff\x00\x00\x00\x00\x08\x00\x01\x00\x68\x74\x62\x00\x1c\x00\x02\x00\x18\x00\x02\x00\x03\x00\x00\x00\x0a\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 23 | # 24 | # m = Message.unpack(aq) 25 | # st = m.service_template 26 | # print m 27 | # print st 28 | # print st.attributes 29 | # print '-'*20 30 | # print len(st.attributes[1].data) 31 | # print struct.unpack('6I', st.attributes[1].data) 32 | # 33 | # print Attr.unpack(aq[44:]) 34 | 35 | def test_unpack_htb_class(self): 36 | """ 37 | TCA_HTB_INIT is composed from TCA_HTB_PARMS, TCA_HTB_CTAB, TCA_HTB_RTAB 38 | 39 | tc class add dev lo parent 1: classid 1:1 htb rate 534 40 | """ 41 | this_dir = os.path.dirname(os.path.realpath(__file__)) 42 | with open(os.path.join(this_dir, 'htb_add_class.data'), 'rb') as f: 43 | data = f.read() 44 | msg = Message.unpack(data) 45 | self.assertEqual(msg.type, RTM_NEWTCLASS) 46 | self.assertEqual(msg.flags, 0x605) 47 | 48 | st = msg.service_template 49 | self.assertEqual(st.tcm_handle, 0x10001) 50 | self.assertEqual(st.tcm_parent, 0x10000) 51 | 52 | init = tuple(st.unpack_attrs(data[36:]))[1] 53 | attrs = list(st.unpack_attrs(init.payload)) 54 | self.assertEqual(len(attrs), 3) 55 | 56 | from shapy.framework.netlink.htb import tc_calc_rtable 57 | self.assertItemsEqual(tc_calc_rtable(66, -1, 1600), 58 | struct.unpack("256I", attrs[2].payload), 59 | "Rate table (rtab) calculation is wrong.") 60 | 61 | 62 | def test_unpack_add_filter(self): 63 | """ 64 | tc filter add dev lo parent 1: protocol ip prio 13 u32 \ 65 | match ip src 127.0.0.3 flowid 1:3 66 | """ 67 | data = "\x5c\x00\x00\x00\x2c\x00\x05\x06\x05\x8e\xce\x4d\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x08\x00\x0d\x00\x08\x00\x01\x00\x75\x33\x32\x00\x30\x00\x02\x00\x08\x00\x01\x00\x03\x00\x01\x00\x24\x00\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x7f\x00\x00\x03\x0c\x00\x00\x00\x00\x00\x00\x00" 68 | msg = Message.unpack(data) 69 | #print msg 70 | st = msg.service_template 71 | attr = st.attributes 72 | #print st 73 | #print attr 74 | 75 | tc_u32_sel = Struct("BBBHHhhI") 76 | tc_u32_key = Struct("IIii") 77 | nested = list(unpack_attrs(attr[1].payload)) 78 | #print nested 79 | #print "{0:#x}".format(struct.unpack("I", nested[0].payload)[0]) 80 | sel = tc_u32_sel.unpack(nested[1].payload[:16]) 81 | key = tc_u32_key.unpack(nested[1].payload[16:]) 82 | #print [ "{0:#x}".format(a) for a in key ] 83 | #print key[1] 84 | #print [ "{0:#x}".format(a) for a in struct.unpack("HH10I", attr[1].payload) ] 85 | 86 | #import pdb; pdb.set_trace() 87 | 88 | def test_unpack_tcp_filter(self): 89 | """ 90 | tc filter add dev lo parent 1: protocol ip prio 1 u32 \ 91 | match ip sport 8000 0xffff flowid 1:5 92 | """ 93 | data = "\\\0\0\0,\0\5\6!\201\333M\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\1\0\10\0\1\0\10\0\1\0u32\0000\0\2\0\10\0\1\0\5\0\1\0$\0\5\0\1\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\0\37@\0\0\24\0\0\0\0\0\0\0" 94 | #data = "\\\0\0\0,\0\5\6\352\210\333M\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\1\0\10\0\1\0\10\0\1\0u32\0000\0\2\0\10\0\1\0\5\0\1\0$\0\5\0\1\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\0\37@\24\0\0\0\0\0\0\0" 95 | msg = Message.unpack(data) 96 | st = msg.service_template 97 | attr = st.attributes 98 | 99 | tc_u32_sel = Struct("BBBHHhhI") 100 | tc_u32_key = Struct("IIii") 101 | nested = list(unpack_attrs(attr[1].payload)) 102 | sel = tc_u32_sel.unpack(nested[1].payload[:16]) 103 | key = tc_u32_key.unpack(nested[1].payload[16:]) 104 | #print sel, key 105 | #print hex_list(sel), hex_list(key) 106 | 107 | def test_unpack_add_redirect_filter(self): 108 | """ 109 | tc filter add dev lo parent 1: protocol ip prio 3 \ 110 | u32 match ip dst 127.0.0.4 flowid 1:1 \ 111 | action mirred egress redirect dev ifb0 112 | 113 | filter parent 1: protocol ip pref 3 u32 114 | filter parent 1: protocol ip pref 3 u32 fh 800: ht divisor 1 115 | filter parent 1: protocol ip pref 3 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:1 116 | match 7f000004/ffffffff at 16 117 | action order 1: mirred (Egress Redirect to device ifb0) stolen 118 | index 2 ref 1 bind 1 119 | """ 120 | data = "\x94\x00\x00\x00\x2c\x00\x05\x06\x4f\x21\xda\x4d\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x08\x00\x03\x00\x08\x00\x01\x00\x75\x33\x32\x00\x68\x00\x02\x00\x08\x00\x01\x00\x01\x00\x01\x00\x38\x00\x07\x00\x34\x00\x01\x00\x0b\x00\x01\x00\x6d\x69\x72\x72\x65\x64\x00\x00\x24\x00\x02\x00\x20\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x24\x00\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x7f\x00\x00\x04\x10\x00\x00\x00\x00\x00\x00\x00" 121 | msg = Message.unpack(data) 122 | st = msg.service_template 123 | attr = st.attributes 124 | 125 | tc_u32_sel = Struct("BBBHHhhI") 126 | tc_u32_key = Struct("IIii") 127 | filter_opt = list(unpack_attrs(attr[1].payload)) # TCA_KIND, TCA_OPTIONS 128 | action = list(unpack_attrs(filter_opt[1].payload)) # TCA_EGRESS_REDIR 129 | mirred = list(unpack_attrs(action[0].payload)) # TCA_ACT_KIND, TCA_ACT_OPTIONS 130 | mirred_parms = list(unpack_attrs(mirred[1].payload)) 131 | 132 | tc_mirred = Struct("IIiiiiI") 133 | #print tc_mirred.unpack(mirred_parms[0].payload) 134 | 135 | 136 | def test_unpack_add_netem(self): 137 | """tc qdisc add dev lo root handle 1: netem delay 10ms""" 138 | data = "\x4c\x00\x00\x00\x24\x00\x05\x06\x07\x1b\xcc\x4d\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\xff\xff\xff\xff\x00\x00\x00\x00\x0a\x00\x01\x00\x6e\x65\x74\x65\x6d\x00\x00\x00\x1c\x00\x02\x00\x5a\x62\x02\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 139 | msg = Message.unpack(data) 140 | self.assertEqual(msg.type, RTM_NEWQDISC) 141 | self.assertEqual(msg.flags, 0x605) 142 | 143 | st = msg.service_template 144 | self.assertAlmostEqual(st.tcm_handle, 0x10000) 145 | self.assertAlmostEqual(st.tcm_parent, 0xffffffff) 146 | 147 | attr, data = NetemOptions.unpack(data[-28:]) 148 | self.assertEqual(attr.data.latency, nl_us2ticks(10*1000)) 149 | 150 | def test_unpack_add_prio(self): 151 | data = "\x4c\x00\x00\x00\x24\x00\x05\x06\x67\x9c\xcd\x4d\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\xff\xff\xff\xff\x00\x00\x00\x00\x09\x00\x01\x00\x70\x72\x69\x6f\x00\x00\x00\x00\x1c\x00\x02\x00\x03\x00\x00\x00\x01\x02\x02\x02\x01\x02\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x04\x00\x02\x00" 152 | msg = Message.unpack(data) 153 | self.assertEqual(msg.type, RTM_NEWQDISC) 154 | self.assertEqual(msg.flags, 0x605) 155 | self.assertEqual(msg.service_template.attributes[1].payload, data[-24:]) 156 | 157 | 158 | def test_unpack_get_stats(self): 159 | """ 160 | tc -s class show dev lo classid 1:1 161 | """ 162 | data1 = "\x24\x00\x00\x00\x2a\x00\x01\x03\x22\x08\xdc\x4d\x00\x00\x00\x00" 163 | data2 = "\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 164 | 165 | nlmsghdr = Struct("IHHII") 166 | tcmsg = Struct("BxxxiIII") 167 | 168 | nlmsghdr.unpack(data1) 169 | tcmsg.unpack(data2) 170 | 171 | 172 | if __name__ == '__main__': 173 | unittest.main() 174 | -------------------------------------------------------------------------------- /shapy/emulation/shaper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | CWC shaper using ShaPy Linux TC library. The basic requirement is that it needs 4 | to aggregate traffic on all specified interfaces to IFB devices and shape it 5 | creating an ilussion of real network. 6 | 7 | Intermediate Function Block is virtual network device used to shape download 8 | speeds. We will set filter for interface we wish to shape download on to forward 9 | all traffic to IFB. There we can shape it as a standard outgoing (egress) 10 | traffic albeit in reality it is incoming (ingress) traffic. 11 | 12 | @author: Petr Praus 13 | """ 14 | import re 15 | import logging 16 | import logging.handlers 17 | 18 | from shapy import settings 19 | from shapy.framework import tcelements as shapy 20 | from shapy.framework import executor 21 | from shapy.framework import utils 22 | 23 | LOG_FILENAME = 'shaper.log' 24 | 25 | shapy_logger = logging.getLogger('shapy') 26 | shapy_logger.setLevel(logging.INFO) 27 | handler = logging.handlers.RotatingFileHandler( 28 | LOG_FILENAME, maxBytes=100*1024, backupCount=5) 29 | fmt = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 30 | handler.setFormatter(fmt) 31 | shapy_logger.addHandler(handler) 32 | 33 | 34 | logger = logging.getLogger('shapy.emulation.shaper') 35 | 36 | class Shaper(object): 37 | instance = None 38 | 39 | def __new__(cls): 40 | if Shaper.instance: 41 | return Shaper.instance 42 | Shaper.instance = super(Shaper, cls).__new__(cls) 43 | return Shaper.instance 44 | 45 | ifb_up = shapy.IFB('ifb1') 46 | ifb_down = shapy.IFB('ifb0') 47 | 48 | # Namespace allocation for handlers: 49 | # 0x1-0x1FE HTB shapers, 0x1FF default HTB, 0x200 - 0x3FF netem 50 | # HTB handles 51 | ifb_up.qhandles = ( '{0:x}'.format(n) for n in xrange(1, 511) ) 52 | ifb_down.qhandles = ( '{0:x}'.format(n) for n in xrange(1, 511) ) 53 | # Netem handles 54 | ifb_up.nhandles = ( '{0:x}'.format(n) for n in xrange(512, 1023) ) 55 | ifb_down.nhandles = ( '{0:x}'.format(n) for n in xrange(512, 1023) ) 56 | 57 | def __setup(self): 58 | # root HTB qdiscs on IFB egress 59 | shapy.IFB.modprobe() 60 | self.ifb_up.add( shapy.HTBQdisc('1:', default_class='1ff') ) 61 | self.ifb_down.add( shapy.HTBQdisc('1:', default_class='1ff') ) 62 | 63 | # add ingress qdiscs to all real interfaces (such as eth or lo) so 64 | # we can redirect traffic to the IFB devices for the actual shaping 65 | for i in settings.EMU_INTERFACES: 66 | interface = shapy.Interface(i) 67 | prioq = shapy.PRIOQdisc('1:') 68 | ingressq = shapy.IngressQdisc() 69 | 70 | # Exclude specified ports from shaping altogether 71 | for port in settings.EMU_NOSHAPE_PORTS: 72 | prioq.add(shapy.FlowFilter('sport %s' % port, '1:1ff', 73 | mask=0xffff, prio=1)) 74 | ingressq.add(shapy.FlowFilter('dport %s' % port, '1:1ff', 75 | mask=0xffff, prio=1)) 76 | 77 | interface.add(prioq) 78 | interface.add_ingress(ingressq) 79 | 80 | 81 | def set_shaping(self, shaping_conf): 82 | """ 83 | shaping_conf format: 84 | { (ip1, ...): { 85 | download: int [kbps/kbit], 86 | upload: int [kbps/kbit], 87 | delay: int [ms]} 88 | } 89 | } 90 | Each IP will have its own separate HTB class. 91 | """ 92 | 93 | for node_ips in shaping_conf: 94 | # if the the user accidentally passes a string instead of one-item 95 | # list, we will do the conversion 96 | if isinstance(node_ips, str): 97 | node_ips = (str,) 98 | if not isinstance(node_ips, (list, tuple)): 99 | logger.warning("IPs must be specified in a list or tuple, skipping!") 100 | continue 101 | 102 | shaping_params = shaping_conf[node_ips] 103 | upload = shaping_params.get('upload', 2**32-1) 104 | download = shaping_params.get('download', 2**32-1) 105 | delay = shaping_params.get('delay', 0) 106 | jitter = shaping_params.get('jitter', 0) 107 | 108 | for ip in node_ips: 109 | utils.validate_ip(ip) 110 | 111 | if ip in self.ip_handles: 112 | logger.info("{0} is already shaped, skipping.".format(ip)) 113 | continue 114 | logger.info("Configuring {0} -> U:{1}{units} D:{2}{units} delay:{3}ms jitter:±{4}ms"\ 115 | .format(ip, upload, download, delay, jitter, units=settings.UNITS)) 116 | # upload shaping for the given set of IPs 117 | self.__shape_upload(ip, upload, delay, jitter) 118 | # download shaping 119 | self.__shape_download(ip, download, delay, jitter) 120 | 121 | # finally, we need to actually run those rules we just created 122 | for i in settings.EMU_INTERFACES: 123 | logger.info("Setting up shaping/emulation on interface {0}:".format(i)) 124 | shapy.Interface(i).set_shaping() 125 | logger.info("Setting up IFB devices:") 126 | self.ifb_up.set_shaping() 127 | self.ifb_down.set_shaping() 128 | 129 | 130 | def teardown_all(self): 131 | """ 132 | This method does not properly reset Shaper object for further use. 133 | It just purges external configuration. Use reset_all() if you intend 134 | to reuse this instance. 135 | """ 136 | for i in settings.EMU_INTERFACES: 137 | logger.info("Tearing down %s" % i) 138 | shapy.Interface(i).teardown() 139 | logger.info("Tearing down IFBs") 140 | shapy.IFB.teardown() 141 | 142 | def reset_all(self): 143 | """ 144 | Completely resets this instance and all associated rules in 145 | underlying OS so you can reuse this instance. 146 | """ 147 | self.teardown_all() 148 | self.__setup() 149 | 150 | def get_traffic(self, ip): 151 | """ 152 | Returns sent/received data for the given IP. This is based on 153 | tc statistics (tc -s) of HTB classes on the respective IFB devices. 154 | """ 155 | if not hasattr(self, 'ip_handles'): 156 | return None, None 157 | 158 | def get_htb_class_send(stats): 159 | try: 160 | return int(re.search(r"Sent ([0-9]+) ", stats).group(1)) 161 | except: 162 | return None, None 163 | 164 | handle = self.ip_handles[ip] 165 | up = executor.get_command('TCStats', interface=self.ifb_up, handle=handle) 166 | down = executor.get_command('TCStats', interface=self.ifb_down, handle=handle) 167 | up_stats = executor.run(up, sudo=False) 168 | down_stats = executor.run(down, sudo=False) 169 | 170 | return get_htb_class_send(up_stats), get_htb_class_send(down_stats) 171 | 172 | 173 | def __init__(self, **kwargs): 174 | # stores the relationship between IP and it's policing HTB classes 175 | self.ip_handles = {} 176 | self.__setup() 177 | 178 | 179 | def __shape_upload(self, ip, rate, delay, jitter): 180 | """Sets up upload shaping (not really policing) and emulation 181 | for the given _ip_.""" 182 | # egress filter to redirect to IFB, upload -> src & ifb1 183 | for i in settings.EMU_INTERFACES: 184 | f = shapy.RedirectFilter('src %s' % ip, self.ifb_up) 185 | shapy.Interface(i).root.add(f) 186 | qh = self.ifb_up.qhandles.next() 187 | nh = self.ifb_up.nhandles.next() 188 | self.ip_handles.update({ip: qh}) 189 | self.__shape_ifb(self.ifb_up, 'src %s' % ip, qh, rate, nh, delay, jitter) 190 | 191 | 192 | def __shape_download(self, ip, rate, delay, jitter): 193 | """Sets up download shaping and emulation for the given _ip_.""" 194 | # ingress filter to redirect to IFB, download -> dst & ifb0 195 | for i in settings.EMU_INTERFACES: 196 | f = shapy.RedirectFilter('dst %s' % ip, self.ifb_down) 197 | shapy.Interface(i).ingress.add(f) 198 | qh = self.ifb_down.qhandles.next() 199 | nh = self.ifb_down.nhandles.next() 200 | self.__shape_ifb(self.ifb_down, 'dst %s' % ip, qh, rate, nh, delay, jitter) 201 | 202 | 203 | def __shape_ifb(self, ifb, ip, qhandle, rate, nhandle, delay, jitter): 204 | """Creates rules on IFB device itself.""" 205 | # filter on the IFB device itself to redirect traffic to a HTB class 206 | ifb.root.add( shapy.FlowFilter(ip, '1:%s' % qhandle) ) 207 | htbq = shapy.HTBClass('1:%s' % qhandle, rate=rate, ceil=rate) 208 | if delay or jitter: 209 | htbq.add(shapy.NetemDelayQdisc('%s:' % nhandle, delay, jitter)) 210 | ifb.root.add( htbq ) 211 | --------------------------------------------------------------------------------