diff options
Diffstat (limited to 'third_party/libwebrtc/tools_webrtc/network_emulator')
3 files changed, 440 insertions, 0 deletions
diff --git a/third_party/libwebrtc/tools_webrtc/network_emulator/config.py b/third_party/libwebrtc/tools_webrtc/network_emulator/config.py new file mode 100644 index 0000000000..9a18bdce45 --- /dev/null +++ b/third_party/libwebrtc/tools_webrtc/network_emulator/config.py @@ -0,0 +1,36 @@ +#!/usr/bin/env vpython3 + +# 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 emulation.""" + + +class ConnectionConfig: + """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) diff --git a/third_party/libwebrtc/tools_webrtc/network_emulator/emulate.py b/third_party/libwebrtc/tools_webrtc/network_emulator/emulate.py new file mode 100755 index 0000000000..a35ccd36c8 --- /dev/null +++ b/third_party/libwebrtc/tools_webrtc/network_emulator/emulate.py @@ -0,0 +1,209 @@ +#!/usr/bin/env vpython3 + +# 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 socket +import sys + +import config +import network_emulator + +_DEFAULT_LOG_LEVEL = logging.INFO + +# Default port range to apply network constraints on. +_DEFAULT_PORT_RANGE = (32768, 65535) + +# The numbers below are gathered from Google stats from the presets of the Apple +# developer tool called Network Link Conditioner. +_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 _GetExternalIp(): + """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 _ParseArgs(): + """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=_DEFAULT_PRESET_ID, + 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 options.preset not in _PRESETS_DICT: + 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.') + + _InitLogging(options.verbose) + return options + + +def _InitLogging(verbose): + """Setup logging.""" + log_level = _DEFAULT_LOG_LEVEL + if verbose: + log_level = logging.DEBUG + logging.basicConfig(level=log_level, format='%(message)s') + + +def main(): + options = _ParseArgs() + + # 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 is not _DEFAULT_PRESET.receive_bw_kbps: + connection_config.receive_bw_kbps = options.receive_bw + if options.send_bw is not _DEFAULT_PRESET.send_bw_kbps: + connection_config.send_bw_kbps = options.send_bw + if options.delay is not _DEFAULT_PRESET.delay_ms: + connection_config.delay_ms = options.delay + if options.packet_loss is not _DEFAULT_PRESET.packet_loss_percent: + connection_config.packet_loss_percent = options.packet_loss + if options.queue is not _DEFAULT_PRESET.queue_slots: + connection_config.queue_slots = options.queue + emulator = network_emulator.NetworkEmulator(connection_config, + options.port_range) + try: + emulator.CheckPermissions() + except network_emulator.NetworkEmulatorError as e: + logging.error('Error: %s\n\nCause: %s', e.fail_msg, e.error) + return -1 + + if not options.target_ip: + external_ip = _GetExternalIp() + else: + external_ip = options.target_ip + + logging.info('Constraining traffic to/from IP: %s', external_ip) + try: + emulator.Emulate(external_ip) + logging.info( + 'Started network emulation 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]) + input('Press Enter to abort Network Emulation...') + logging.info('Flushing all Dummynet rules...') + network_emulator.Cleanup() + logging.info('Completed Network Emulation.') + return 0 + except network_emulator.NetworkEmulatorError as e: + logging.error('Error: %s\n\nCause: %s', e.fail_msg, e.error) + return -2 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/third_party/libwebrtc/tools_webrtc/network_emulator/network_emulator.py b/third_party/libwebrtc/tools_webrtc/network_emulator/network_emulator.py new file mode 100644 index 0000000000..a7776a5f92 --- /dev/null +++ b/third_party/libwebrtc/tools_webrtc/network_emulator/network_emulator.py @@ -0,0 +1,195 @@ +#!/usr/bin/env vpython3 + +# 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: + """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._CreateDummynetPipe( + 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._CreateDummynetPipe( + 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._CreateDummynetRule(receive_pipe_id, 'any', + target_ip, self._port_range) + logging.debug('Created incoming rule: %s', incoming_rule_id) + outgoing_rule_id = self._CreateDummynetRule(send_pipe_id, target_ip, 'any', + self._port_range) + logging.debug('Created outgoing rule: %s', outgoing_rule_id) + + @staticmethod + def CheckPermissions(): + """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 as permission_error: + + # AttributeError will be raised on Windows. + if ctypes.windll.shell32.IsUserAnAdmin() == 0: + raise NetworkEmulatorError('You must run this script with administrator' + ' privileges.') from permission_error + + def _CreateDummynetRule(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 + ] + _RunIpfwCommand(add_part + ['src-port', '%s-%s' % port_range], + 'Failed to add Dummynet src-port rule.') + _RunIpfwCommand(add_part + ['dst-port', '%s-%s' % port_range], + 'Failed to add Dummynet dst-port rule.') + return self._rule_counter + + def _CreateDummynetPipe(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).') + _RunIpfwCommand(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. + """ + _RunIpfwCommand(['-f', 'flush'], 'Failed to flush Dummynet rules!') + _RunIpfwCommand(['-f', 'pipe', 'flush'], 'Failed to flush Dummynet pipes!') + + +def _RunIpfwCommand(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() |