1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
|
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Traffic control library for constraining the network configuration on a port.
The traffic controller sets up a constrained network configuration on a port.
Traffic to the constrained port is forwarded to a specified server port.
"""
import logging
import os
import re
import subprocess
# The maximum bandwidth limit.
_DEFAULT_MAX_BANDWIDTH_KBIT = 1000000
class TrafficControlError(BaseException):
"""Exception raised for errors in traffic control library.
Attributes:
msg: User defined error message.
cmd: Command for which the exception was raised.
returncode: Return code of running the command.
stdout: Output of running the command.
stderr: Error output of running the command.
"""
def __init__(self, msg, cmd=None, returncode=None, output=None,
error=None):
BaseException.__init__(self, msg)
self.msg = msg
self.cmd = cmd
self.returncode = returncode
self.output = output
self.error = error
def CheckRequirements():
"""Checks if permissions are available to run traffic control commands.
Raises:
TrafficControlError: If permissions to run traffic control commands are not
available.
"""
if os.geteuid() != 0:
_Exec(['sudo', '-n', 'tc', '-help'],
msg=('Cannot run \'tc\' command. Traffic Control must be run as root '
'or have password-less sudo access to this command.'))
_Exec(['sudo', '-n', 'iptables', '-help'],
msg=('Cannot run \'iptables\' command. Traffic Control must be run '
'as root or have password-less sudo access to this command.'))
def CreateConstrainedPort(config):
"""Creates a new constrained port.
Imposes packet level constraints such as bandwidth, latency, and packet loss
on a given port using the specified configuration dictionary. Traffic to that
port is forwarded to a specified server port.
Args:
config: Constraint configuration dictionary, format:
port: Port to constrain (integer 1-65535).
server_port: Port to redirect traffic on [port] to (integer 1-65535).
interface: Network interface name (string).
latency: Delay added on each packet sent (integer in ms).
bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
loss: Percentage of packets to drop (integer 0-100).
Raises:
TrafficControlError: If any operation fails. The message in the exception
describes what failed.
"""
_CheckArgsExist(config, 'interface', 'port', 'server_port')
_AddRootQdisc(config['interface'])
try:
_ConfigureClass('add', config)
_AddSubQdisc(config)
_AddFilter(config['interface'], config['port'])
_AddIptableRule(config['interface'], config['port'], config['server_port'])
except TrafficControlError as e:
logging.debug('Error creating constrained port %d.\nError: %s\n'
'Deleting constrained port.', config['port'], e.error)
DeleteConstrainedPort(config)
raise e
def DeleteConstrainedPort(config):
"""Deletes an existing constrained port.
Deletes constraints set on a given port and the traffic forwarding rule from
the constrained port to a specified server port.
The original constrained network configuration used to create the constrained
port must be passed in.
Args:
config: Constraint configuration dictionary, format:
port: Port to constrain (integer 1-65535).
server_port: Port to redirect traffic on [port] to (integer 1-65535).
interface: Network interface name (string).
bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
Raises:
TrafficControlError: If any operation fails. The message in the exception
describes what failed.
"""
_CheckArgsExist(config, 'interface', 'port', 'server_port')
try:
# Delete filters first so it frees the class.
_DeleteFilter(config['interface'], config['port'])
finally:
try:
# Deleting the class deletes attached qdisc as well.
_ConfigureClass('del', config)
finally:
_DeleteIptableRule(config['interface'], config['port'],
config['server_port'])
def TearDown(config):
"""Deletes the root qdisc and all iptables rules.
Args:
config: Constraint configuration dictionary, format:
interface: Network interface name (string).
Raises:
TrafficControlError: If any operation fails. The message in the exception
describes what failed.
"""
_CheckArgsExist(config, 'interface')
command = ['sudo', 'tc', 'qdisc', 'del', 'dev', config['interface'], 'root']
try:
_Exec(command, msg='Could not delete root qdisc.')
finally:
_DeleteAllIpTableRules()
def _CheckArgsExist(config, *args):
"""Check that the args exist in config dictionary and are not None.
Args:
config: Any dictionary.
*args: The list of key names to check.
Raises:
TrafficControlError: If any key name does not exist in config or is None.
"""
for key in args:
if key not in config.keys() or config[key] is None:
raise TrafficControlError('Missing "%s" parameter.' % key)
def _AddRootQdisc(interface):
"""Sets up the default root qdisc.
Args:
interface: Network interface name.
Raises:
TrafficControlError: If adding the root qdisc fails for a reason other than
it already exists.
"""
command = ['sudo', 'tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle',
'1:', 'htb']
try:
_Exec(command, msg=('Error creating root qdisc. '
'Make sure you have root access'))
except TrafficControlError as e:
# Ignore the error if root already exists.
if not 'File exists' in e.error:
raise e
def _ConfigureClass(option, config):
"""Adds or deletes a class and qdisc attached to the root.
The class specifies bandwidth, and qdisc specifies delay and packet loss. The
class ID is based on the config port.
Args:
option: Adds or deletes a class option [add|del].
config: Constraint configuration dictionary, format:
port: Port to constrain (integer 1-65535).
interface: Network interface name (string).
bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
"""
# Use constrained port as class ID so we can attach the qdisc and filter to
# it, as well as delete the class, using only the port number.
class_id = '1:%x' % config['port']
if 'bandwidth' not in config.keys() or not config['bandwidth']:
bandwidth = _DEFAULT_MAX_BANDWIDTH_KBIT
else:
bandwidth = config['bandwidth']
bandwidth = '%dkbit' % bandwidth
command = ['sudo', 'tc', 'class', option, 'dev', config['interface'],
'parent', '1:', 'classid', class_id, 'htb', 'rate', bandwidth,
'ceil', bandwidth]
_Exec(command, msg=('Error configuring class ID %s using "%s" command.' %
(class_id, option)))
def _AddSubQdisc(config):
"""Adds a qdisc attached to the class identified by the config port.
Args:
config: Constraint configuration dictionary, format:
port: Port to constrain (integer 1-65535).
interface: Network interface name (string).
latency: Delay added on each packet sent (integer in ms).
loss: Percentage of packets to drop (integer 0-100).
"""
port_hex = '%x' % config['port']
class_id = '1:%x' % config['port']
command = ['sudo', 'tc', 'qdisc', 'add', 'dev', config['interface'], 'parent',
class_id, 'handle', port_hex + ':0', 'netem']
# Check if packet-loss is set in the configuration.
if 'loss' in config.keys() and config['loss']:
loss = '%d%%' % config['loss']
command.extend(['loss', loss])
# Check if latency is set in the configuration.
if 'latency' in config.keys() and config['latency']:
latency = '%dms' % config['latency']
command.extend(['delay', latency])
_Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id)
def _AddFilter(interface, port):
"""Redirects packets coming to a specified port into the constrained class.
Args:
interface: Interface name to attach the filter to (string).
port: Port number to filter packets with (integer 1-65535).
"""
class_id = '1:%x' % port
command = ['sudo', 'tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip',
'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port,
'0xffff', 'flowid', class_id]
_Exec(command, msg='Error adding filter on port %d.' % port)
def _DeleteFilter(interface, port):
"""Deletes the filter attached to the configured port.
Args:
interface: Interface name the filter is attached to (string).
port: Port number being filtered (integer 1-65535).
"""
handle_id = _GetFilterHandleId(interface, port)
command = ['sudo', 'tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip',
'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32']
_Exec(command, msg='Error deleting filter on port %d.' % port)
def _GetFilterHandleId(interface, port):
"""Searches for the handle ID of the filter identified by the config port.
Args:
interface: Interface name the filter is attached to (string).
port: Port number being filtered (integer 1-65535).
Returns:
The handle ID.
Raises:
TrafficControlError: If handle ID was not found.
"""
command = ['sudo', 'tc', 'filter', 'list', 'dev', interface, 'parent', '1:']
output = _Exec(command, msg='Error listing filters.')
# Search for the filter handle ID associated with class ID '1:port'.
handle_id_re = re.search(
'([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output)
if handle_id_re:
return handle_id_re.group(1)
raise TrafficControlError(('Could not find filter handle ID for class ID '
'1:%x.') % port)
def _AddIptableRule(interface, port, server_port):
"""Forwards traffic from constrained port to a specified server port.
Args:
interface: Interface name to attach the filter to (string).
port: Port of incoming packets (integer 1-65535).
server_port: Server port to forward the packets to (integer 1-65535).
"""
# Preroute rules for accessing the port through external connections.
command = ['sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING', '-i',
interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
'--to-port', server_port]
_Exec(command, msg='Error adding iptables rule for port %d.' % port)
# Output rules for accessing the rule through localhost or 127.0.0.1
command = ['sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp',
'--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
_Exec(command, msg='Error adding iptables rule for port %d.' % port)
def _DeleteIptableRule(interface, port, server_port):
"""Deletes the iptable rule associated with specified port number.
Args:
interface: Interface name to attach the filter to (string).
port: Port of incoming packets (integer 1-65535).
server_port: Server port packets are forwarded to (integer 1-65535).
"""
command = ['sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING', '-i',
interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
'--to-port', server_port]
_Exec(command, msg='Error deleting iptables rule for port %d.' % port)
command = ['sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp',
'--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
_Exec(command, msg='Error adding iptables rule for port %d.' % port)
def _DeleteAllIpTableRules():
"""Deletes all iptables rules."""
command = ['sudo', 'iptables', '-t', 'nat', '-F']
_Exec(command, msg='Error deleting all iptables rules.')
def _Exec(command, msg=None):
"""Executes a command.
Args:
command: Command list to execute.
msg: Message describing the error in case the command fails.
Returns:
The standard output from running the command.
Raises:
TrafficControlError: If command fails. Message is set by the msg parameter.
"""
cmd_list = [str(x) for x in command]
cmd = ' '.join(cmd_list)
logging.debug('Running command: %s', cmd)
p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = p.communicate()
if p.returncode != 0:
raise TrafficControlError(msg, cmd, p.returncode, output, error)
return output.strip()
|