| #!/usr/bin/env python |
| # Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. |
| # |
| # Use of this source code is governed by a BSD-style license |
| # that can be found in the LICENSE file in the root of the source |
| # tree. An additional intellectual property rights grant can be found |
| # in the file PATENTS. All contributing project authors may |
| # be found in the AUTHORS file in the root of the source tree. |
| |
| """Script for constraining traffic on the local machine.""" |
| |
| import ctypes |
| import logging |
| import os |
| import subprocess |
| import sys |
| |
| |
| class NetworkEmulatorError(BaseException): |
| """Exception raised for errors in the network emulator. |
| |
| Attributes: |
| fail_msg: User defined error message. |
| cmd: Command for which the exception was raised. |
| returncode: Return code of running the command. |
| stdout: Output of running the command. |
| stderr: Error output of running the command. |
| """ |
| |
| def __init__(self, fail_msg, cmd=None, returncode=None, output=None, |
| error=None): |
| BaseException.__init__(self, fail_msg) |
| self.fail_msg = fail_msg |
| self.cmd = cmd |
| self.returncode = returncode |
| self.output = output |
| self.error = error |
| |
| |
| class NetworkEmulator(object): |
| """A network emulator that can constrain the network using Dummynet.""" |
| |
| def __init__(self, connection_config, port_range): |
| """Constructor. |
| |
| Args: |
| connection_config: A config.ConnectionConfig object containing the |
| characteristics for the connection to be emulation. |
| port_range: Tuple containing two integers defining the port range. |
| """ |
| self._pipe_counter = 0 |
| self._rule_counter = 0 |
| self._port_range = port_range |
| self._connection_config = connection_config |
| |
| def emulate(self, target_ip): |
| """Starts a network emulation by setting up Dummynet rules. |
| |
| Args: |
| target_ip: The IP address of the interface that shall be that have the |
| network constraints applied to it. |
| """ |
| receive_pipe_id = self._create_dummynet_pipe( |
| self._connection_config.receive_bw_kbps, |
| self._connection_config.delay_ms, |
| self._connection_config.packet_loss_percent, |
| self._connection_config.queue_slots) |
| logging.debug('Created receive pipe: %s', receive_pipe_id) |
| send_pipe_id = self._create_dummynet_pipe( |
| self._connection_config.send_bw_kbps, |
| self._connection_config.delay_ms, |
| self._connection_config.packet_loss_percent, |
| self._connection_config.queue_slots) |
| logging.debug('Created send pipe: %s', send_pipe_id) |
| |
| # Adding the rules will start the emulation. |
| incoming_rule_id = self._create_dummynet_rule(receive_pipe_id, 'any', |
| target_ip, self._port_range) |
| logging.debug('Created incoming rule: %s', incoming_rule_id) |
| outgoing_rule_id = self._create_dummynet_rule(send_pipe_id, target_ip, |
| 'any', self._port_range) |
| logging.debug('Created outgoing rule: %s', outgoing_rule_id) |
| |
| @staticmethod |
| def check_permissions(): |
| """Checks if permissions are available to run Dummynet commands. |
| |
| Raises: |
| NetworkEmulatorError: If permissions to run Dummynet commands are not |
| available. |
| """ |
| try: |
| if os.getuid() != 0: |
| raise NetworkEmulatorError('You must run this script with sudo.') |
| except AttributeError: |
| |
| # AttributeError will be raised on Windows. |
| if ctypes.windll.shell32.IsUserAnAdmin() == 0: |
| raise NetworkEmulatorError('You must run this script with administrator' |
| ' privileges.') |
| |
| def _create_dummynet_rule(self, pipe_id, from_address, to_address, |
| port_range): |
| """Creates a network emulation rule and returns its ID. |
| |
| Args: |
| pipe_id: integer ID of the pipe. |
| from_address: The IP address to match source address. May be an IP or |
| 'any'. |
| to_address: The IP address to match destination address. May be an IP or |
| 'any'. |
| port_range: The range of ports the rule shall be applied on. Must be |
| specified as a tuple of with two integers. |
| Returns: |
| The ID of the rule, starting at 100. The rule ID increments with 100 for |
| each rule being added. |
| """ |
| self._rule_counter += 100 |
| add_part = ['add', self._rule_counter, 'pipe', pipe_id, |
| 'ip', 'from', from_address, 'to', to_address] |
| _run_ipfw_command(add_part + ['src-port', '%s-%s' % port_range], |
| 'Failed to add Dummynet src-port rule.') |
| _run_ipfw_command(add_part + ['dst-port', '%s-%s' % port_range], |
| 'Failed to add Dummynet dst-port rule.') |
| return self._rule_counter |
| |
| def _create_dummynet_pipe(self, bandwidth_kbps, delay_ms, packet_loss_percent, |
| queue_slots): |
| """Creates a Dummynet pipe and return its ID. |
| |
| Args: |
| bandwidth_kbps: Bandwidth. |
| delay_ms: Delay for a one-way trip of a packet. |
| packet_loss_percent: Float value of packet loss, in percent. |
| queue_slots: Size of the queue. |
| Returns: |
| The ID of the pipe, starting at 1. |
| """ |
| self._pipe_counter += 1 |
| cmd = ['pipe', self._pipe_counter, 'config', |
| 'bw', str(bandwidth_kbps/8) + 'KByte/s', |
| 'delay', '%sms' % delay_ms, |
| 'plr', (packet_loss_percent/100.0), |
| 'queue', queue_slots] |
| error_message = 'Failed to create Dummynet pipe. ' |
| if sys.platform.startswith('linux'): |
| error_message += ('Make sure you have loaded the ipfw_mod.ko module to ' |
| 'your kernel (sudo insmod /path/to/ipfw_mod.ko).') |
| _run_ipfw_command(cmd, error_message) |
| return self._pipe_counter |
| |
| def cleanup(): |
| """Stops the network emulation by flushing all Dummynet rules. |
| |
| Notice that this will flush any rules that may have been created previously |
| before starting the emulation. |
| """ |
| _run_ipfw_command(['-f', 'flush'], |
| 'Failed to flush Dummynet rules!') |
| _run_ipfw_command(['-f', 'pipe', 'flush'], |
| 'Failed to flush Dummynet pipes!') |
| |
| def _run_ipfw_command(command, fail_msg=None): |
| """Executes a command and prefixes the appropriate command for |
| Windows or Linux/UNIX. |
| |
| Args: |
| command: Command list to execute. |
| fail_msg: Message describing the error in case the command fails. |
| |
| Raises: |
| NetworkEmulatorError: If command fails a message is set by the fail_msg |
| parameter. |
| """ |
| if sys.platform == 'win32': |
| ipfw_command = ['ipfw.exe'] |
| else: |
| ipfw_command = ['sudo', '-n', 'ipfw'] |
| |
| cmd_list = ipfw_command[:] + [str(x) for x in command] |
| cmd_string = ' '.join(cmd_list) |
| logging.debug('Running command: %s', cmd_string) |
| process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| output, error = process.communicate() |
| if process.returncode != 0: |
| raise NetworkEmulatorError(fail_msg, cmd_string, process.returncode, output, |
| error) |
| return output.strip() |