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