Network simulation script based on Dummynet.
This script only intends to support Mac and Linux so far. Additional coding and conditional checking will be required to support Windows. BUG=None. TEST=Tested on Linux and Mac. Review URL: https://webrtc-codereview.appspot.com/606006 git-svn-id: http://webrtc.googlecode.com/svn/trunk@2335 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
36
tools/network_simulator/config.py
Normal file
36
tools/network_simulator/config.py
Normal file
@ -0,0 +1,36 @@
|
||||
#!/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.
|
||||
|
||||
"""Configuration class for network simulation."""
|
||||
|
||||
|
||||
class ConnectionConfig(object):
|
||||
"""Configuration containing the characteristics of a network connection."""
|
||||
|
||||
def __init__(self, num, name, receive_bw_kbps, send_bw_kbps, delay_ms,
|
||||
packet_loss_percent, queue_slots):
|
||||
self.num = num
|
||||
self.name = name
|
||||
self.receive_bw_kbps = receive_bw_kbps
|
||||
self.send_bw_kbps = send_bw_kbps
|
||||
self.delay_ms = delay_ms
|
||||
self.packet_loss_percent = packet_loss_percent
|
||||
self.queue_slots = queue_slots
|
||||
|
||||
def __str__(self):
|
||||
"""String representing the configuration.
|
||||
|
||||
Returns:
|
||||
A string formatted and padded like this example:
|
||||
12 Name 375 kbps 375 kbps 10 145 ms 0.1 %
|
||||
"""
|
||||
left_aligned_name = self.name.ljust(24, ' ')
|
||||
return '%2s %24s %5s kbps %5s kbps %4s %5s ms %3s %%' % (
|
||||
self.num, left_aligned_name, self.receive_bw_kbps, self.send_bw_kbps,
|
||||
self.queue_slots, self.delay_ms, self.packet_loss_percent)
|
173
tools/network_simulator/network_simulator.py
Normal file
173
tools/network_simulator/network_simulator.py
Normal file
@ -0,0 +1,173 @@
|
||||
#!/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 logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class NetworkSimulatorError(BaseException):
|
||||
"""Exception raised for errors in the network simulator.
|
||||
|
||||
Attributes:
|
||||
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, msg, cmd=None, returncode=None, output=None,
|
||||
error=None):
|
||||
BaseException.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.cmd = cmd
|
||||
self.returncode = returncode
|
||||
self.output = output
|
||||
self.error = error
|
||||
|
||||
|
||||
class NetworkSimulator(object):
|
||||
"""A network simulator 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 simulated.
|
||||
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 simulate(self, target_ip):
|
||||
"""Starts a network simulation 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 simulation.
|
||||
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)
|
||||
|
||||
def check_permissions(self):
|
||||
"""Checks if permissions are available to run Dummynet commands.
|
||||
|
||||
Raises:
|
||||
NetworkSimulatorError: If permissions to run Dummynet commands are not
|
||||
available.
|
||||
"""
|
||||
if os.geteuid() != 0:
|
||||
self._run_shell_command(
|
||||
['sudo', '-n', 'ipfw', '-h'],
|
||||
msg=('Cannot run \'ipfw\' command. This script must be run as '
|
||||
'root or have password-less sudo access to this command.'))
|
||||
|
||||
def cleanup(self):
|
||||
"""Stops the network simulation by flushing all Dummynet rules.
|
||||
|
||||
Notice that this will flush any rules that may have been created previously
|
||||
before starting the simulation.
|
||||
"""
|
||||
self._run_shell_command(['sudo', 'ipfw', '-f', 'flush'],
|
||||
'Failed to flush Dummynet rules!')
|
||||
|
||||
def _create_dummynet_rule(self, pipe_id, from_address, to_address,
|
||||
port_range):
|
||||
"""Creates a network simulation 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 = ['sudo', 'ipfw', 'add', self._rule_counter, 'pipe', pipe_id,
|
||||
'ip', 'from', from_address, 'to', to_address]
|
||||
self._run_shell_command(add_part + ['src-port', '%s-%s' % port_range],
|
||||
'Failed to add Dummynet src-port rule.')
|
||||
self._run_shell_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 = ['sudo', 'ipfw', '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]
|
||||
self._run_shell_command(cmd, 'Failed to create Dummynet pipe')
|
||||
return self._pipe_counter
|
||||
|
||||
def _run_shell_command(self, command, msg=None):
|
||||
"""Executes a command.
|
||||
|
||||
Args:
|
||||
command: Command list to execute.
|
||||
msg: Message describing the error in case the command fails.
|
||||
|
||||
Returns:
|
||||
The standard output from running the command.
|
||||
|
||||
Raises:
|
||||
NetworkSimulatorError: If command fails. Message is set by the msg
|
||||
parameter.
|
||||
"""
|
||||
cmd_list = [str(x) for x in command]
|
||||
cmd = ' '.join(cmd_list)
|
||||
logging.debug('Running command: %s', cmd)
|
||||
|
||||
process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
output, error = process.communicate()
|
||||
if process.returncode != 0:
|
||||
raise NetworkSimulatorError(msg, cmd, process.returncode, output, error)
|
||||
return output.strip()
|
194
tools/network_simulator/simulate.py
Executable file
194
tools/network_simulator/simulate.py
Executable file
@ -0,0 +1,194 @@
|
||||
#!/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 logging
|
||||
import optparse
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import config
|
||||
import network_simulator
|
||||
|
||||
_DEFAULT_LOG_LEVEL = logging.INFO
|
||||
|
||||
# Default port range to apply network constraints on.
|
||||
_DEFAULT_PORT_RANGE = (30000, 65535)
|
||||
|
||||
_PRESETS = [
|
||||
config.ConnectionConfig(1, 'Generic, Bad', 95, 95, 250, 2, 100),
|
||||
config.ConnectionConfig(2, 'Generic, Average', 375, 375, 145, 0.1, 100),
|
||||
config.ConnectionConfig(3, 'Generic, Good', 1000, 1000, 35, 0, 100),
|
||||
config.ConnectionConfig(4, '3G, Average Case', 780, 330, 100, 0, 100),
|
||||
config.ConnectionConfig(5, '3G, Good', 850, 420, 90, 0, 100),
|
||||
config.ConnectionConfig(6, '3G, Lossy Network', 780, 330, 100, 1, 100),
|
||||
config.ConnectionConfig(7, 'Cable Modem', 6000, 1000, 2, 0, 10),
|
||||
config.ConnectionConfig(8, 'DSL', 2000, 256, 5, 0, 10),
|
||||
config.ConnectionConfig(9, 'Edge, Average Case', 240, 200, 400, 0, 100),
|
||||
config.ConnectionConfig(10, 'Edge, Good', 250, 200, 350, 0, 100),
|
||||
config.ConnectionConfig(11, 'Edge, Lossy Network', 240, 200, 400, 1, 100),
|
||||
config.ConnectionConfig(12, 'Wifi, Average Case', 40000, 33000, 1, 0, 100),
|
||||
config.ConnectionConfig(13, 'Wifi, Good', 45000, 40000, 1, 0, 100),
|
||||
config.ConnectionConfig(14, 'Wifi, Lossy', 40000, 33000, 1, 0, 100),
|
||||
]
|
||||
_PRESETS_DICT = dict((p.num, p) for p in _PRESETS)
|
||||
|
||||
_DEFAULT_PRESET_ID = 2
|
||||
_DEFAULT_PRESET = _PRESETS_DICT[_DEFAULT_PRESET_ID]
|
||||
|
||||
|
||||
class NonStrippingEpilogOptionParser(optparse.OptionParser):
|
||||
"""Custom parser to let us show the epilog without weird line breaking."""
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
return self.epilog
|
||||
|
||||
|
||||
def _get_external_ip():
|
||||
"""Finds out the machine's external IP by connecting to google.com."""
|
||||
external_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
external_socket.connect(('google.com', 80))
|
||||
return external_socket.getsockname()[0]
|
||||
|
||||
|
||||
def _parse_args():
|
||||
"""Define and parse the command-line arguments."""
|
||||
presets_string = '\n'.join(str(p) for p in _PRESETS)
|
||||
parser = NonStrippingEpilogOptionParser(epilog=(
|
||||
'\nAvailable presets:\n'
|
||||
' Bandwidth (kbps) Packet\n'
|
||||
'ID Name Receive Send Queue Delay loss \n'
|
||||
'-- ---- --------- -------- ----- ------- ------\n'
|
||||
'%s\n' % presets_string))
|
||||
parser.add_option('-p', '--preset', type='int', default=2,
|
||||
help=('ConnectionConfig configuration, specified by ID. '
|
||||
'Default: %default'))
|
||||
parser.add_option('-r', '--receive-bw', type='int',
|
||||
default=_DEFAULT_PRESET.receive_bw_kbps,
|
||||
help=('Receive bandwidth in kilobit/s. Default: %default'))
|
||||
parser.add_option('-s', '--send-bw', type='int',
|
||||
default=_DEFAULT_PRESET.send_bw_kbps,
|
||||
help=('Send bandwidth in kilobit/s. Default: %default'))
|
||||
parser.add_option('-d', '--delay', type='int',
|
||||
default=_DEFAULT_PRESET.delay_ms,
|
||||
help=('Delay in ms. Default: %default'))
|
||||
parser.add_option('-l', '--packet-loss', type='float',
|
||||
default=_DEFAULT_PRESET.packet_loss_percent,
|
||||
help=('Packet loss in %. Default: %default'))
|
||||
parser.add_option('-q', '--queue', type='int',
|
||||
default=_DEFAULT_PRESET.queue_slots,
|
||||
help=('Queue size as number of slots. Default: %default'))
|
||||
parser.add_option('--port-range', default='%s,%s' % _DEFAULT_PORT_RANGE,
|
||||
help=('Range of ports for constrained network. Specify as '
|
||||
'two comma separated integers. Default: %default'))
|
||||
parser.add_option('--target-ip', default=None,
|
||||
help=('The interface IP address to apply the rules for. '
|
||||
'Default: the external facing interface IP address.'))
|
||||
parser.add_option('-v', '--verbose', action='store_true', default=False,
|
||||
help=('Turn on verbose output. Will print all \'ipfw\' '
|
||||
'commands that are executed.'))
|
||||
|
||||
options = parser.parse_args()[0]
|
||||
|
||||
# Find preset by ID, if specified:
|
||||
if options.preset and not _PRESETS_DICT.has_key(options.preset):
|
||||
parser.error('Invalid preset: %s' % options.preset)
|
||||
|
||||
# Simple validation of the IP address, if supplied:
|
||||
if options.target_ip:
|
||||
try:
|
||||
socket.inet_aton(options.target_ip)
|
||||
except socket.error:
|
||||
parser.error('Invalid IP address specified: %s' % options.target_ip)
|
||||
|
||||
# Convert port range into the desired tuple format.
|
||||
try:
|
||||
if isinstance(options.port_range, str):
|
||||
options.port_range = tuple(int(port) for port in
|
||||
options.port_range.split(','))
|
||||
if len(options.port_range) != 2:
|
||||
parser.error('Invalid port range specified, please specify two '
|
||||
'integers separated by a comma.')
|
||||
except ValueError:
|
||||
parser.error('Invalid port range specified.')
|
||||
|
||||
_set_logger(options.verbose)
|
||||
return options
|
||||
|
||||
|
||||
def _set_logger(verbose):
|
||||
"""Setup logging."""
|
||||
log_level = _DEFAULT_LOG_LEVEL
|
||||
if verbose:
|
||||
log_level = logging.DEBUG
|
||||
logging.basicConfig(level=log_level, format='%(message)s')
|
||||
|
||||
|
||||
def _main():
|
||||
"""Checks arguments, permissions and runs a network simulation."""
|
||||
if os.name != 'posix':
|
||||
print >> sys.stderr, 'This script is only supported on Linux and Mac.'
|
||||
return 1
|
||||
|
||||
options = _parse_args()
|
||||
|
||||
# Build a configuration object. Override any preset configuration settings if
|
||||
# a value of a setting was also given as a flag.
|
||||
connection_config = _PRESETS_DICT[options.preset]
|
||||
if options.receive_bw:
|
||||
connection_config.receive_bw_kbps = options.receive_bw
|
||||
if options.send_bw:
|
||||
connection_config.send_bw_kbps = options.send_bw
|
||||
if options.delay:
|
||||
connection_config.delay_ms = options.delay
|
||||
if options.packet_loss:
|
||||
connection_config.packet_loss_percent = options.packet_loss
|
||||
if options.queue:
|
||||
connection_config.queue_slots = options.queue
|
||||
|
||||
simulator = network_simulator.NetworkSimulator(connection_config,
|
||||
options.port_range)
|
||||
try:
|
||||
simulator.check_permissions()
|
||||
except network_simulator.NetworkSimulatorError as e:
|
||||
logging.error('Error: %s\n\nCause: %s', e.msg, e.error)
|
||||
return -1
|
||||
|
||||
if not options.target_ip:
|
||||
external_ip = _get_external_ip()
|
||||
else:
|
||||
external_ip = options.target_ip
|
||||
|
||||
logging.info('Simulating traffic to/from IP: %s', external_ip)
|
||||
simulator.simulate(external_ip)
|
||||
logging.info('Started network simulation with the following configuration:\n'
|
||||
' Receive bandwidth: %s kbps (%s kB/s)\n'
|
||||
' Send bandwidth : %s kbps (%s kB/s)\n'
|
||||
' Delay : %s ms\n'
|
||||
' Packet loss : %s %%\n'
|
||||
' Queue slots : %s',
|
||||
connection_config.receive_bw_kbps,
|
||||
connection_config.receive_bw_kbps/8,
|
||||
connection_config.send_bw_kbps,
|
||||
connection_config.send_bw_kbps/8,
|
||||
connection_config.delay_ms,
|
||||
connection_config.packet_loss_percent,
|
||||
connection_config.queue_slots)
|
||||
logging.info('Affected traffic: IP traffic on ports %s-%s',
|
||||
options.port_range[0], options.port_range[1])
|
||||
raw_input('Press Enter to abort Network simulation...')
|
||||
logging.info('Flushing all Dummynet rules...')
|
||||
simulator.cleanup()
|
||||
logging.info('Completed Network Simulation.')
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(_main())
|
Reference in New Issue
Block a user