2012-05-31 22:19:05 +02:00
|
|
|
#!/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."""
|
|
|
|
|
2013-03-08 11:50:14 +01:00
|
|
|
import ctypes
|
2012-05-31 22:19:05 +02:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import subprocess
|
2012-06-01 10:42:17 +02:00
|
|
|
import sys
|
2012-05-31 22:19:05 +02:00
|
|
|
|
|
|
|
|
2012-06-01 10:42:17 +02:00
|
|
|
class NetworkEmulatorError(BaseException):
|
|
|
|
"""Exception raised for errors in the network emulator.
|
2012-05-31 22:19:05 +02:00
|
|
|
|
|
|
|
Attributes:
|
2013-03-08 14:43:36 +01:00
|
|
|
fail_msg: User defined error message.
|
2012-05-31 22:19:05 +02:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2013-03-08 14:43:36 +01:00
|
|
|
def __init__(self, fail_msg, cmd=None, returncode=None, output=None,
|
2012-05-31 22:19:05 +02:00
|
|
|
error=None):
|
2013-03-08 14:43:36 +01:00
|
|
|
BaseException.__init__(self, fail_msg)
|
|
|
|
self.fail_msg = fail_msg
|
2012-05-31 22:19:05 +02:00
|
|
|
self.cmd = cmd
|
|
|
|
self.returncode = returncode
|
|
|
|
self.output = output
|
|
|
|
self.error = error
|
|
|
|
|
|
|
|
|
2012-06-01 10:42:17 +02:00
|
|
|
class NetworkEmulator(object):
|
|
|
|
"""A network emulator that can constrain the network using Dummynet."""
|
2012-05-31 22:19:05 +02:00
|
|
|
|
|
|
|
def __init__(self, connection_config, port_range):
|
|
|
|
"""Constructor.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
connection_config: A config.ConnectionConfig object containing the
|
2012-06-01 10:42:17 +02:00
|
|
|
characteristics for the connection to be emulation.
|
2012-05-31 22:19:05 +02:00
|
|
|
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
|
|
|
|
|
2012-06-01 10:42:17 +02:00
|
|
|
def emulate(self, target_ip):
|
|
|
|
"""Starts a network emulation by setting up Dummynet rules.
|
2012-05-31 22:19:05 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2012-06-01 10:42:17 +02:00
|
|
|
# Adding the rules will start the emulation.
|
2012-05-31 22:19:05 +02:00
|
|
|
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)
|
|
|
|
|
2013-03-07 10:59:43 +01:00
|
|
|
@staticmethod
|
|
|
|
def check_permissions():
|
2012-05-31 22:19:05 +02:00
|
|
|
"""Checks if permissions are available to run Dummynet commands.
|
|
|
|
|
|
|
|
Raises:
|
2012-06-01 10:42:17 +02:00
|
|
|
NetworkEmulatorError: If permissions to run Dummynet commands are not
|
2012-05-31 22:19:05 +02:00
|
|
|
available.
|
|
|
|
"""
|
2013-03-08 11:50:14 +01:00
|
|
|
try:
|
|
|
|
if os.getuid() != 0:
|
|
|
|
raise NetworkEmulatorError('You must run this script with sudo.')
|
|
|
|
except AttributeError:
|
2012-05-31 22:19:05 +02:00
|
|
|
|
2013-03-08 11:50:14 +01:00
|
|
|
# AttributeError will be raised on Windows.
|
|
|
|
if ctypes.windll.shell32.IsUserAnAdmin() == 0:
|
|
|
|
raise NetworkEmulatorError('You must run this script with administrator'
|
2013-03-11 15:52:56 +01:00
|
|
|
' privileges.')
|
2013-03-08 11:50:14 +01:00
|
|
|
|
2012-05-31 22:19:05 +02:00
|
|
|
def _create_dummynet_rule(self, pipe_id, from_address, to_address,
|
|
|
|
port_range):
|
2012-06-01 10:42:17 +02:00
|
|
|
"""Creates a network emulation rule and returns its ID.
|
2012-05-31 22:19:05 +02:00
|
|
|
|
|
|
|
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
|
2013-03-08 11:50:14 +01:00
|
|
|
add_part = ['add', self._rule_counter, 'pipe', pipe_id,
|
2012-05-31 22:19:05 +02:00
|
|
|
'ip', 'from', from_address, 'to', to_address]
|
2013-03-08 14:43:36 +01:00
|
|
|
_run_ipfw_command(add_part + ['src-port', '%s-%s' % port_range],
|
2013-03-08 11:50:14 +01:00
|
|
|
'Failed to add Dummynet src-port rule.')
|
2013-03-08 14:43:36 +01:00
|
|
|
_run_ipfw_command(add_part + ['dst-port', '%s-%s' % port_range],
|
2013-03-08 11:50:14 +01:00
|
|
|
'Failed to add Dummynet dst-port rule.')
|
2012-05-31 22:19:05 +02:00
|
|
|
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
|
2013-03-08 11:50:14 +01:00
|
|
|
cmd = ['pipe', self._pipe_counter, 'config',
|
2012-05-31 22:19:05 +02:00
|
|
|
'bw', str(bandwidth_kbps/8) + 'KByte/s',
|
|
|
|
'delay', '%sms' % delay_ms,
|
|
|
|
'plr', (packet_loss_percent/100.0),
|
|
|
|
'queue', queue_slots]
|
2012-06-01 10:42:17 +02:00
|
|
|
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 '
|
2013-03-08 11:50:14 +01:00
|
|
|
'your kernel (sudo insmod /path/to/ipfw_mod.ko).')
|
2013-03-08 14:43:36 +01:00
|
|
|
_run_ipfw_command(cmd, error_message)
|
2012-05-31 22:19:05 +02:00
|
|
|
return self._pipe_counter
|
|
|
|
|
2013-03-08 14:43:36 +01:00
|
|
|
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!')
|
2012-05-31 22:19:05 +02:00
|
|
|
|
2013-03-08 14:43:36 +01:00
|
|
|
def _run_ipfw_command(command, fail_msg=None):
|
|
|
|
"""Executes a command and prefixes the appropriate command for
|
|
|
|
Windows or Linux/UNIX.
|
2012-05-31 22:19:05 +02:00
|
|
|
|
2013-03-07 10:59:43 +01:00
|
|
|
Args:
|
|
|
|
command: Command list to execute.
|
2013-03-08 14:43:36 +01:00
|
|
|
fail_msg: Message describing the error in case the command fails.
|
2012-05-31 22:19:05 +02:00
|
|
|
|
2013-03-08 14:43:36 +01:00
|
|
|
Raises:
|
2013-03-11 15:52:56 +01:00
|
|
|
NetworkEmulatorError: If command fails a message is set by the fail_msg
|
2013-03-08 14:43:36 +01:00
|
|
|
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()
|