#!/usr/bin/env python
# Copyright 2010 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import platformsettings
import re
# Mac has broken bandwitdh parsing, so double check the values.
# On Mac OS X 10.6, "KBit/s" actually uses "KByte/s".
BANDWIDTH_PATTERN = r'0|\d+[KM]?(bit|Byte)/s'
class TrafficShaperException(Exception):
pass
class BandwidthValueError(TrafficShaperException):
def __init__(self, value): # pylint: disable=super-init-not-called
self.value = value
def __str__(self):
return 'Value, "%s", does not match regex: %s' % (
self.value, BANDWIDTH_PATTERN)
class TrafficShaper(object):
"""Manages network traffic shaping."""
# Pick webpagetest-compatible values (details: http://goo.gl/oghTg).
_UPLOAD_PIPE = '10' # Enforces overall upload bandwidth.
_UPLOAD_QUEUE = '10' # Shares upload bandwidth among source ports.
_UPLOAD_RULE = '5000' # Specifies when the upload queue is used.
_DOWNLOAD_PIPE = '11' # Enforces overall download bandwidth.
_DOWNLOAD_QUEUE = '11' # Shares download bandwidth among destination ports.
_DOWNLOAD_RULE = '5100' # Specifies when the download queue is used.
_QUEUE_SLOTS = 100 # Number of packets to queue.
_BANDWIDTH_RE = re.compile(BANDWIDTH_PATTERN)
def __init__(self,
dont_use=None,
host='127.0.0.1',
ports=None,
up_bandwidth='0',
down_bandwidth='0',
delay_ms='0',
packet_loss_rate='0',
init_cwnd='0',
use_loopback=True):
"""Start shaping traffic.
Args:
host: a host string (name or IP) for the web proxy.
ports: a list of ports to shape traffic on.
up_bandwidth: Upload bandwidth
down_bandwidth: Download bandwidth
Bandwidths measured in [K|M]{bit/s|Byte/s}. '0' means unlimited.
delay_ms: Propagation delay in milliseconds. '0' means no delay.
packet_loss_rate: Packet loss rate in range [0..1]. '0' means no loss.
init_cwnd: the initial cwnd setting. '0' means no change.
use_loopback: True iff shaping is done on the loopback (or equiv) adapter.
"""
assert dont_use is None # Force args to be named.
self.host = host
self.ports = ports
self.up_bandwidth = up_bandwidth
self.down_bandwidth = down_bandwidth
self.delay_ms = delay_ms
self.packet_loss_rate = packet_loss_rate
self.init_cwnd = init_cwnd
self.use_loopback = use_loopback
if not self._BANDWIDTH_RE.match(self.up_bandwidth):
raise BandwidthValueError(self.up_bandwidth)
if not self._BANDWIDTH_RE.match(self.down_bandwidth):
raise BandwidthValueError(self.down_bandwidth)
self.is_shaping = False
def __enter__(self):
if self.use_loopback:
platformsettings.setup_temporary_loopback_config()
if self.init_cwnd != '0':
platformsettings.set_temporary_tcp_init_cwnd(self.init_cwnd)
try:
ipfw_list = platformsettings.ipfw('list')
if not ipfw_list.startswith('65535 '):
logging.warn('ipfw has existing rules:\n%s', ipfw_list)
self._delete_rules(ipfw_list)
except Exception:
pass
if (self.up_bandwidth == '0' and self.down_bandwidth == '0' and
self.delay_ms == '0' and self.packet_loss_rate == '0'):
logging.info('Skipped shaping traffic.')
return
if not self.ports:
raise TrafficShaperException('No ports on which to shape traffic.')
ports = ','.join(str(p) for p in self.ports)
half_delay_ms = int(self.delay_ms) / 2 # split over up/down links
try:
# Configure upload shaping.
platformsettings.ipfw(
'pipe', self._UPLOAD_PIPE,
'config',
'bw', self.up_bandwidth,
'delay', half_delay_ms,
)
platformsettings.ipfw(
'queue', self._UPLOAD_QUEUE,
'config',
'pipe', self._UPLOAD_PIPE,
'plr', self.packet_loss_rate,
'queue', self._QUEUE_SLOTS,
'mask', 'src-port', '0xffff',
)
platformsettings.ipfw(
'add', self._UPLOAD_RULE,
'queue', self._UPLOAD_QUEUE,
'ip',
'from', 'any',
'to', self.host,
self.use_loopback and 'out' or 'in',
'dst-port', ports,
)
self.is_shaping = True
# Configure download shaping.
platformsettings.ipfw(
'pipe', self._DOWNLOAD_PIPE,
'config',
'bw', self.down_bandwidth,
'delay', half_delay_ms,
)
platformsettings.ipfw(
'queue', self._DOWNLOAD_QUEUE,
'config',
'pipe', self._DOWNLOAD_PIPE,
'plr', self.packet_loss_rate,
'queue', self._QUEUE_SLOTS,
'mask', 'dst-port', '0xffff',
)
platformsettings.ipfw(
'add', self._DOWNLOAD_RULE,
'queue', self._DOWNLOAD_QUEUE,
'ip',
'from', self.host,
'to', 'any',
'out',
'src-port', ports,
)
logging.info('Started shaping traffic')
except Exception:
logging.error('Unable to shape traffic.')
raise
def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
if self.is_shaping:
try:
self._delete_rules()
logging.info('Stopped shaping traffic')
except Exception:
logging.error('Unable to stop shaping traffic.')
raise
def _delete_rules(self, ipfw_list=None):
if ipfw_list is None:
ipfw_list = platformsettings.ipfw('list')
existing_rules = set(
r.split()[0].lstrip('0') for r in ipfw_list.splitlines())
delete_rules = [r for r in (self._DOWNLOAD_RULE, self._UPLOAD_RULE)
if r in existing_rules]
if delete_rules:
platformsettings.ipfw('delete', *delete_rules)