1#!/usr/bin/env python 2# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. 3# 4# Use of this source code is governed by a BSD-style license 5# that can be found in the LICENSE file in the root of the source 6# tree. An additional intellectual property rights grant can be found 7# in the file PATENTS. All contributing project authors may 8# be found in the AUTHORS file in the root of the source tree. 9 10"""Script for constraining traffic on the local machine.""" 11 12 13import logging 14import optparse 15import socket 16import sys 17 18import config 19import network_emulator 20 21 22_DEFAULT_LOG_LEVEL = logging.INFO 23 24# Default port range to apply network constraints on. 25_DEFAULT_PORT_RANGE = (32768, 65535) 26 27# The numbers below are gathered from Google stats from the presets of the Apple 28# developer tool called Network Link Conditioner. 29_PRESETS = [ 30 config.ConnectionConfig(1, 'Generic, Bad', 95, 95, 250, 2, 100), 31 config.ConnectionConfig(2, 'Generic, Average', 375, 375, 145, 0.1, 100), 32 config.ConnectionConfig(3, 'Generic, Good', 1000, 1000, 35, 0, 100), 33 config.ConnectionConfig(4, '3G, Average Case', 780, 330, 100, 0, 100), 34 config.ConnectionConfig(5, '3G, Good', 850, 420, 90, 0, 100), 35 config.ConnectionConfig(6, '3G, Lossy Network', 780, 330, 100, 1, 100), 36 config.ConnectionConfig(7, 'Cable Modem', 6000, 1000, 2, 0, 10), 37 config.ConnectionConfig(8, 'DSL', 2000, 256, 5, 0, 10), 38 config.ConnectionConfig(9, 'Edge, Average Case', 240, 200, 400, 0, 100), 39 config.ConnectionConfig(10, 'Edge, Good', 250, 200, 350, 0, 100), 40 config.ConnectionConfig(11, 'Edge, Lossy Network', 240, 200, 400, 1, 100), 41 config.ConnectionConfig(12, 'Wifi, Average Case', 40000, 33000, 1, 0, 100), 42 config.ConnectionConfig(13, 'Wifi, Good', 45000, 40000, 1, 0, 100), 43 config.ConnectionConfig(14, 'Wifi, Lossy', 40000, 33000, 1, 0, 100), 44 ] 45_PRESETS_DICT = dict((p.num, p) for p in _PRESETS) 46 47_DEFAULT_PRESET_ID = 2 48_DEFAULT_PRESET = _PRESETS_DICT[_DEFAULT_PRESET_ID] 49 50 51class NonStrippingEpilogOptionParser(optparse.OptionParser): 52 """Custom parser to let us show the epilog without weird line breaking.""" 53 54 def format_epilog(self, formatter): 55 return self.epilog 56 57 58def _get_external_ip(): 59 """Finds out the machine's external IP by connecting to google.com.""" 60 external_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 61 external_socket.connect(('google.com', 80)) 62 return external_socket.getsockname()[0] 63 64 65def _parse_args(): 66 """Define and parse the command-line arguments.""" 67 presets_string = '\n'.join(str(p) for p in _PRESETS) 68 parser = NonStrippingEpilogOptionParser(epilog=( 69 '\nAvailable presets:\n' 70 ' Bandwidth (kbps) Packet\n' 71 'ID Name Receive Send Queue Delay loss \n' 72 '-- ---- --------- -------- ----- ------- ------\n' 73 '%s\n' % presets_string)) 74 parser.add_option('-p', '--preset', type='int', default=_DEFAULT_PRESET_ID, 75 help=('ConnectionConfig configuration, specified by ID. ' 76 'Default: %default')) 77 parser.add_option('-r', '--receive-bw', type='int', 78 default=_DEFAULT_PRESET.receive_bw_kbps, 79 help=('Receive bandwidth in kilobit/s. Default: %default')) 80 parser.add_option('-s', '--send-bw', type='int', 81 default=_DEFAULT_PRESET.send_bw_kbps, 82 help=('Send bandwidth in kilobit/s. Default: %default')) 83 parser.add_option('-d', '--delay', type='int', 84 default=_DEFAULT_PRESET.delay_ms, 85 help=('Delay in ms. Default: %default')) 86 parser.add_option('-l', '--packet-loss', type='float', 87 default=_DEFAULT_PRESET.packet_loss_percent, 88 help=('Packet loss in %. Default: %default')) 89 parser.add_option('-q', '--queue', type='int', 90 default=_DEFAULT_PRESET.queue_slots, 91 help=('Queue size as number of slots. Default: %default')) 92 parser.add_option('--port-range', default='%s,%s' % _DEFAULT_PORT_RANGE, 93 help=('Range of ports for constrained network. Specify as ' 94 'two comma separated integers. Default: %default')) 95 parser.add_option('--target-ip', default=None, 96 help=('The interface IP address to apply the rules for. ' 97 'Default: the external facing interface IP address.')) 98 parser.add_option('-v', '--verbose', action='store_true', default=False, 99 help=('Turn on verbose output. Will print all \'ipfw\' ' 100 'commands that are executed.')) 101 102 options = parser.parse_args()[0] 103 104 # Find preset by ID, if specified. 105 if options.preset and not _PRESETS_DICT.has_key(options.preset): 106 parser.error('Invalid preset: %s' % options.preset) 107 108 # Simple validation of the IP address, if supplied. 109 if options.target_ip: 110 try: 111 socket.inet_aton(options.target_ip) 112 except socket.error: 113 parser.error('Invalid IP address specified: %s' % options.target_ip) 114 115 # Convert port range into the desired tuple format. 116 try: 117 if isinstance(options.port_range, str): 118 options.port_range = tuple(int(port) for port in 119 options.port_range.split(',')) 120 if len(options.port_range) != 2: 121 parser.error('Invalid port range specified, please specify two ' 122 'integers separated by a comma.') 123 except ValueError: 124 parser.error('Invalid port range specified.') 125 126 _set_logger(options.verbose) 127 return options 128 129 130def _set_logger(verbose): 131 """Setup logging.""" 132 log_level = _DEFAULT_LOG_LEVEL 133 if verbose: 134 log_level = logging.DEBUG 135 logging.basicConfig(level=log_level, format='%(message)s') 136 137 138def _main(): 139 options = _parse_args() 140 141 # Build a configuration object. Override any preset configuration settings if 142 # a value of a setting was also given as a flag. 143 connection_config = _PRESETS_DICT[options.preset] 144 if options.receive_bw is not _DEFAULT_PRESET.receive_bw_kbps: 145 connection_config.receive_bw_kbps = options.receive_bw 146 if options.send_bw is not _DEFAULT_PRESET.send_bw_kbps: 147 connection_config.send_bw_kbps = options.send_bw 148 if options.delay is not _DEFAULT_PRESET.delay_ms: 149 connection_config.delay_ms = options.delay 150 if options.packet_loss is not _DEFAULT_PRESET.packet_loss_percent: 151 connection_config.packet_loss_percent = options.packet_loss 152 if options.queue is not _DEFAULT_PRESET.queue_slots: 153 connection_config.queue_slots = options.queue 154 emulator = network_emulator.NetworkEmulator(connection_config, 155 options.port_range) 156 try: 157 emulator.check_permissions() 158 except network_emulator.NetworkEmulatorError as e: 159 logging.error('Error: %s\n\nCause: %s', e.fail_msg, e.error) 160 return -1 161 162 if not options.target_ip: 163 external_ip = _get_external_ip() 164 else: 165 external_ip = options.target_ip 166 167 logging.info('Constraining traffic to/from IP: %s', external_ip) 168 try: 169 emulator.emulate(external_ip) 170 logging.info('Started network emulation with the following configuration:\n' 171 ' Receive bandwidth: %s kbps (%s kB/s)\n' 172 ' Send bandwidth : %s kbps (%s kB/s)\n' 173 ' Delay : %s ms\n' 174 ' Packet loss : %s %%\n' 175 ' Queue slots : %s', 176 connection_config.receive_bw_kbps, 177 connection_config.receive_bw_kbps/8, 178 connection_config.send_bw_kbps, 179 connection_config.send_bw_kbps/8, 180 connection_config.delay_ms, 181 connection_config.packet_loss_percent, 182 connection_config.queue_slots) 183 logging.info('Affected traffic: IP traffic on ports %s-%s', 184 options.port_range[0], options.port_range[1]) 185 raw_input('Press Enter to abort Network Emulation...') 186 logging.info('Flushing all Dummynet rules...') 187 network_emulator.cleanup() 188 logging.info('Completed Network Emulation.') 189 return 0 190 except network_emulator.NetworkEmulatorError as e: 191 logging.error('Error: %s\n\nCause: %s', e.fail_msg, e.error) 192 return -2 193 194if __name__ == '__main__': 195 sys.exit(_main()) 196