Home | History | Annotate | Download | only in web-page-replay
      1 #!/usr/bin/env python
      2 # Copyright 2010 Google Inc. All Rights Reserved.
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 
     16 import logging
     17 import platformsettings
     18 import re
     19 
     20 
     21 # Mac has broken bandwitdh parsing, so double check the values.
     22 # On Mac OS X 10.6, "KBit/s" actually uses "KByte/s".
     23 BANDWIDTH_PATTERN = r'0|\d+[KM]?(bit|Byte)/s'
     24 
     25 
     26 class TrafficShaperException(Exception):
     27   pass
     28 
     29 
     30 class BandwidthValueError(TrafficShaperException):
     31   def __init__(self, value):  # pylint: disable=super-init-not-called
     32     self.value = value
     33 
     34   def __str__(self):
     35     return 'Value, "%s", does not match regex: %s' % (
     36         self.value, BANDWIDTH_PATTERN)
     37 
     38 
     39 class TrafficShaper(object):
     40   """Manages network traffic shaping."""
     41 
     42   # Pick webpagetest-compatible values (details: http://goo.gl/oghTg).
     43   _UPLOAD_PIPE = '10'      # Enforces overall upload bandwidth.
     44   _UPLOAD_QUEUE = '10'     # Shares upload bandwidth among source ports.
     45   _UPLOAD_RULE = '5000'    # Specifies when the upload queue is used.
     46   _DOWNLOAD_PIPE = '11'    # Enforces overall download bandwidth.
     47   _DOWNLOAD_QUEUE = '11'   # Shares download bandwidth among destination ports.
     48   _DOWNLOAD_RULE = '5100'  # Specifies when the download queue is used.
     49   _QUEUE_SLOTS = 100       # Number of packets to queue.
     50 
     51   _BANDWIDTH_RE = re.compile(BANDWIDTH_PATTERN)
     52 
     53   def __init__(self,
     54                dont_use=None,
     55                host='127.0.0.1',
     56                ports=None,
     57                up_bandwidth='0',
     58                down_bandwidth='0',
     59                delay_ms='0',
     60                packet_loss_rate='0',
     61                init_cwnd='0',
     62                use_loopback=True):
     63     """Start shaping traffic.
     64 
     65     Args:
     66       host: a host string (name or IP) for the web proxy.
     67       ports: a list of ports to shape traffic on.
     68       up_bandwidth: Upload bandwidth
     69       down_bandwidth: Download bandwidth
     70            Bandwidths measured in [K|M]{bit/s|Byte/s}. '0' means unlimited.
     71       delay_ms: Propagation delay in milliseconds. '0' means no delay.
     72       packet_loss_rate: Packet loss rate in range [0..1]. '0' means no loss.
     73       init_cwnd: the initial cwnd setting. '0' means no change.
     74       use_loopback: True iff shaping is done on the loopback (or equiv) adapter.
     75     """
     76     assert dont_use is None  # Force args to be named.
     77     self.host = host
     78     self.ports = ports
     79     self.up_bandwidth = up_bandwidth
     80     self.down_bandwidth = down_bandwidth
     81     self.delay_ms = delay_ms
     82     self.packet_loss_rate = packet_loss_rate
     83     self.init_cwnd = init_cwnd
     84     self.use_loopback = use_loopback
     85     if not self._BANDWIDTH_RE.match(self.up_bandwidth):
     86       raise BandwidthValueError(self.up_bandwidth)
     87     if not self._BANDWIDTH_RE.match(self.down_bandwidth):
     88       raise BandwidthValueError(self.down_bandwidth)
     89     self.is_shaping = False
     90 
     91   def __enter__(self):
     92     if self.use_loopback:
     93       platformsettings.setup_temporary_loopback_config()
     94     if self.init_cwnd != '0':
     95       platformsettings.set_temporary_tcp_init_cwnd(self.init_cwnd)
     96     try:
     97       ipfw_list = platformsettings.ipfw('list')
     98       if not ipfw_list.startswith('65535 '):
     99         logging.warn('ipfw has existing rules:\n%s', ipfw_list)
    100         self._delete_rules(ipfw_list)
    101     except Exception:
    102       pass
    103     if (self.up_bandwidth == '0' and self.down_bandwidth == '0' and
    104         self.delay_ms == '0' and self.packet_loss_rate == '0'):
    105       logging.info('Skipped shaping traffic.')
    106       return
    107     if not self.ports:
    108       raise TrafficShaperException('No ports on which to shape traffic.')
    109 
    110     ports = ','.join(str(p) for p in self.ports)
    111     half_delay_ms = int(self.delay_ms) / 2  # split over up/down links
    112 
    113     try:
    114       # Configure upload shaping.
    115       platformsettings.ipfw(
    116           'pipe', self._UPLOAD_PIPE,
    117           'config',
    118           'bw', self.up_bandwidth,
    119           'delay', half_delay_ms,
    120           )
    121       platformsettings.ipfw(
    122           'queue', self._UPLOAD_QUEUE,
    123           'config',
    124           'pipe', self._UPLOAD_PIPE,
    125           'plr', self.packet_loss_rate,
    126           'queue', self._QUEUE_SLOTS,
    127           'mask', 'src-port', '0xffff',
    128           )
    129       platformsettings.ipfw(
    130           'add', self._UPLOAD_RULE,
    131           'queue', self._UPLOAD_QUEUE,
    132           'ip',
    133           'from', 'any',
    134           'to', self.host,
    135           self.use_loopback and 'out' or 'in',
    136           'dst-port', ports,
    137           )
    138       self.is_shaping = True
    139 
    140       # Configure download shaping.
    141       platformsettings.ipfw(
    142           'pipe', self._DOWNLOAD_PIPE,
    143           'config',
    144           'bw', self.down_bandwidth,
    145           'delay', half_delay_ms,
    146           )
    147       platformsettings.ipfw(
    148           'queue', self._DOWNLOAD_QUEUE,
    149           'config',
    150           'pipe', self._DOWNLOAD_PIPE,
    151           'plr', self.packet_loss_rate,
    152           'queue', self._QUEUE_SLOTS,
    153           'mask', 'dst-port', '0xffff',
    154           )
    155       platformsettings.ipfw(
    156           'add', self._DOWNLOAD_RULE,
    157           'queue', self._DOWNLOAD_QUEUE,
    158           'ip',
    159           'from', self.host,
    160           'to', 'any',
    161           'out',
    162           'src-port', ports,
    163           )
    164       logging.info('Started shaping traffic')
    165     except Exception:
    166       logging.error('Unable to shape traffic.')
    167       raise
    168 
    169   def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
    170     if self.is_shaping:
    171       try:
    172         self._delete_rules()
    173         logging.info('Stopped shaping traffic')
    174       except Exception:
    175         logging.error('Unable to stop shaping traffic.')
    176         raise
    177 
    178   def _delete_rules(self, ipfw_list=None):
    179     if ipfw_list is None:
    180       ipfw_list = platformsettings.ipfw('list')
    181     existing_rules = set(
    182         r.split()[0].lstrip('0') for r in ipfw_list.splitlines())
    183     delete_rules = [r for r in (self._DOWNLOAD_RULE, self._UPLOAD_RULE)
    184                     if r in existing_rules]
    185     if delete_rules:
    186       platformsettings.ipfw('delete', *delete_rules)
    187