Home | History | Annotate | Download | only in network_emulator
      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 
     13 import logging
     14 import optparse
     15 import socket
     16 import sys
     17 
     18 import config
     19 import 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 
     51 class 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 
     58 def _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 
     65 def _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 
    130 def _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 
    138 def _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 
    194 if __name__ == '__main__':
    195   sys.exit(_main())
    196