2 # Copyright 2010 Google Inc. All Rights Reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 import platformsettings
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'
26 class TrafficShaperException(Exception):
30 class BandwidthValueError(TrafficShaperException
):
31 def __init__(self
, value
): # pylint: disable=super-init-not-called
35 return 'Value, "%s", does not match regex: %s' % (
36 self
.value
, BANDWIDTH_PATTERN
)
39 class TrafficShaper(object):
40 """Manages network traffic shaping."""
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.
51 _BANDWIDTH_RE
= re
.compile(BANDWIDTH_PATTERN
)
63 """Start shaping traffic.
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.
76 assert dont_use
is None # Force args to be named.
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
93 platformsettings
.setup_temporary_loopback_config()
94 if self
.init_cwnd
!= '0':
95 platformsettings
.set_temporary_tcp_init_cwnd(self
.init_cwnd
)
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
)
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.')
108 raise TrafficShaperException('No ports on which to shape traffic.')
110 ports
= ','.join(str(p
) for p
in self
.ports
)
111 half_delay_ms
= int(self
.delay_ms
) / 2 # split over up/down links
114 # Configure upload shaping.
115 platformsettings
.ipfw(
116 'pipe', self
._UPLOAD
_PIPE
,
118 'bw', self
.up_bandwidth
,
119 'delay', half_delay_ms
,
121 platformsettings
.ipfw(
122 'queue', self
._UPLOAD
_QUEUE
,
124 'pipe', self
._UPLOAD
_PIPE
,
125 'plr', self
.packet_loss_rate
,
126 'queue', self
._QUEUE
_SLOTS
,
127 'mask', 'src-port', '0xffff',
129 platformsettings
.ipfw(
130 'add', self
._UPLOAD
_RULE
,
131 'queue', self
._UPLOAD
_QUEUE
,
135 self
.use_loopback
and 'out' or 'in',
138 self
.is_shaping
= True
140 # Configure download shaping.
141 platformsettings
.ipfw(
142 'pipe', self
._DOWNLOAD
_PIPE
,
144 'bw', self
.down_bandwidth
,
145 'delay', half_delay_ms
,
147 platformsettings
.ipfw(
148 'queue', self
._DOWNLOAD
_QUEUE
,
150 'pipe', self
._DOWNLOAD
_PIPE
,
151 'plr', self
.packet_loss_rate
,
152 'queue', self
._QUEUE
_SLOTS
,
153 'mask', 'dst-port', '0xffff',
155 platformsettings
.ipfw(
156 'add', self
._DOWNLOAD
_RULE
,
157 'queue', self
._DOWNLOAD
_QUEUE
,
164 logging
.info('Started shaping traffic')
166 logging
.error('Unable to shape traffic.')
169 def __exit__(self
, unused_exc_type
, unused_exc_val
, unused_exc_tb
):
173 logging
.info('Stopped shaping traffic')
175 logging
.error('Unable to stop shaping traffic.')
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
]
186 platformsettings
.ipfw('delete', *delete_rules
)