15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Copyright (c) 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""Traffic control library for constraining the network configuration on a port.
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)The traffic controller sets up a constrained network configuration on a port.
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)Traffic to the constrained port is forwarded to a specified server port.
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import logging
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import os
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import re
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import subprocess
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# The maximum bandwidth limit.
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)_DEFAULT_MAX_BANDWIDTH_KBIT = 1000000
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class TrafficControlError(BaseException):
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Exception raised for errors in traffic control library.
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Attributes:
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    msg: User defined error message.
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    cmd: Command for which the exception was raised.
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    returncode: Return code of running the command.
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    stdout: Output of running the command.
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    stderr: Error output of running the command.
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def __init__(self, msg, cmd=None, returncode=None, output=None,
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)               error=None):
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    BaseException.__init__(self, msg)
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.msg = msg
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.cmd = cmd
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.returncode = returncode
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.output = output
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.error = error
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def CheckRequirements():
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Checks if permissions are available to run traffic control commands.
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If permissions to run traffic control commands are not
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    available.
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if os.geteuid() != 0:
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _Exec(['sudo', '-n', 'tc', '-help'],
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          msg=('Cannot run \'tc\' command. Traffic Control must be run as root '
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)               'or have password-less sudo access to this command.'))
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _Exec(['sudo', '-n', 'iptables', '-help'],
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          msg=('Cannot run \'iptables\' command. Traffic Control must be run '
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)               'as root or have password-less sudo access to this command.'))
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def CreateConstrainedPort(config):
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Creates a new constrained port.
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Imposes packet level constraints such as bandwidth, latency, and packet loss
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  on a given port using the specified configuration dictionary. Traffic to that
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  port is forwarded to a specified server port.
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Constraint configuration dictionary, format:
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      port: Port to constrain (integer 1-65535).
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      server_port: Port to redirect traffic on [port] to (integer 1-65535).
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      interface: Network interface name (string).
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      latency: Delay added on each packet sent (integer in ms).
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      loss: Percentage of packets to drop (integer 0-100).
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If any operation fails. The message in the exception
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    describes what failed.
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _CheckArgsExist(config, 'interface', 'port', 'server_port')
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _AddRootQdisc(config['interface'])
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  try:
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _ConfigureClass('add', config)
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _AddSubQdisc(config)
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _AddFilter(config['interface'], config['port'])
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _AddIptableRule(config['interface'], config['port'], config['server_port'])
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  except TrafficControlError as e:
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    logging.debug('Error creating constrained port %d.\nError: %s\n'
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                  'Deleting constrained port.', config['port'], e.error)
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    DeleteConstrainedPort(config)
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise e
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def DeleteConstrainedPort(config):
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Deletes an existing constrained port.
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Deletes constraints set on a given port and the traffic forwarding rule from
965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  the constrained port to a specified server port.
975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  The original constrained network configuration used to create the constrained
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  port must be passed in.
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Constraint configuration dictionary, format:
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      port: Port to constrain (integer 1-65535).
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      server_port: Port to redirect traffic on [port] to (integer 1-65535).
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      interface: Network interface name (string).
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If any operation fails. The message in the exception
1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    describes what failed.
1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _CheckArgsExist(config, 'interface', 'port', 'server_port')
1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  try:
1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # Delete filters first so it frees the class.
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _DeleteFilter(config['interface'], config['port'])
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  finally:
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    try:
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # Deleting the class deletes attached qdisc as well.
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      _ConfigureClass('del', config)
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    finally:
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      _DeleteIptableRule(config['interface'], config['port'],
1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                         config['server_port'])
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def TearDown(config):
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Deletes the root qdisc and all iptables rules.
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Constraint configuration dictionary, format:
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      interface: Network interface name (string).
1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If any operation fails. The message in the exception
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    describes what failed.
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _CheckArgsExist(config, 'interface')
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'qdisc', 'del', 'dev', config['interface'], 'root']
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  try:
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _Exec(command, msg='Could not delete root qdisc.')
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  finally:
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _DeleteAllIpTableRules()
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _CheckArgsExist(config, *args):
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Check that the args exist in config dictionary and are not None.
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Any dictionary.
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    *args: The list of key names to check.
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If any key name does not exist in config or is None.
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for key in args:
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if key not in config.keys() or config[key] is None:
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      raise TrafficControlError('Missing "%s" parameter.' % key)
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _AddRootQdisc(interface):
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Sets up the default root qdisc.
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Network interface name.
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If adding the root qdisc fails for a reason other than
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    it already exists.
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle',
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '1:', 'htb']
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  try:
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _Exec(command, msg=('Error creating root qdisc. '
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        'Make sure you have root access'))
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  except TrafficControlError as e:
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # Ignore the error if root already exists.
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if not 'File exists' in e.error:
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      raise e
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _ConfigureClass(option, config):
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Adds or deletes a class and qdisc attached to the root.
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  The class specifies bandwidth, and qdisc specifies delay and packet loss. The
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  class ID is based on the config port.
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    option: Adds or deletes a class option [add|del].
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Constraint configuration dictionary, format:
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      port: Port to constrain (integer 1-65535).
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      interface: Network interface name (string).
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Use constrained port as class ID so we can attach the qdisc and filter to
1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # it, as well as delete the class, using only the port number.
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  class_id = '1:%x' % config['port']
1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if 'bandwidth' not in config.keys() or not config['bandwidth']:
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    bandwidth = _DEFAULT_MAX_BANDWIDTH_KBIT
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else:
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    bandwidth = config['bandwidth']
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  bandwidth = '%dkbit' % bandwidth
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'class', option, 'dev', config['interface'],
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             'parent', '1:', 'classid', class_id, 'htb', 'rate', bandwidth,
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             'ceil', bandwidth]
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg=('Error configuring class ID %s using "%s" command.' %
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                      (class_id, option)))
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _AddSubQdisc(config):
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Adds a qdisc attached to the class identified by the config port.
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    config: Constraint configuration dictionary, format:
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      port: Port to constrain (integer 1-65535).
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      interface: Network interface name (string).
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      latency: Delay added on each packet sent (integer in ms).
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      loss: Percentage of packets to drop (integer 0-100).
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  port_hex = '%x' % config['port']
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  class_id = '1:%x' % config['port']
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'qdisc', 'add', 'dev', config['interface'], 'parent',
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             class_id, 'handle', port_hex + ':0', 'netem']
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Check if packet-loss is set in the configuration.
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if 'loss' in config.keys() and config['loss']:
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    loss = '%d%%' % config['loss']
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    command.extend(['loss', loss])
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Check if latency is set in the configuration.
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if 'latency' in config.keys() and config['latency']:
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    latency = '%dms' % config['latency']
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    command.extend(['delay', latency])
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id)
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _AddFilter(interface, port):
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Redirects packets coming to a specified port into the constrained class.
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Interface name to attach the filter to (string).
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    port: Port number to filter packets with (integer 1-65535).
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  class_id = '1:%x' % port
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip',
2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port,
2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '0xffff', 'flowid', class_id]
2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error adding filter on port %d.' % port)
2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _DeleteFilter(interface, port):
2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Deletes the filter attached to the configured port.
2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Interface name the filter is attached to (string).
2575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    port: Port number being filtered (integer 1-65535).
2585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
2595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  handle_id = _GetFilterHandleId(interface, port)
2605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip',
2615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32']
2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error deleting filter on port %d.' % port)
2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _GetFilterHandleId(interface, port):
2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Searches for the handle ID of the filter identified by the config port.
2675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Interface name the filter is attached to (string).
2705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    port: Port number being filtered (integer 1-65535).
2715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Returns:
2735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    The handle ID.
2745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
2765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If handle ID was not found.
2775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
2785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'tc', 'filter', 'list', 'dev', interface, 'parent', '1:']
2795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  output = _Exec(command, msg='Error listing filters.')
2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Search for the filter handle ID associated with class ID '1:port'.
2815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  handle_id_re = re.search(
2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output)
2835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if handle_id_re:
2845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return handle_id_re.group(1)
2855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  raise TrafficControlError(('Could not find filter handle ID for class ID '
2865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                             '1:%x.') % port)
2875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _AddIptableRule(interface, port, server_port):
2905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Forwards traffic from constrained port to a specified server port.
2915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
2935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Interface name to attach the filter to (string).
2945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    port: Port of incoming packets (integer 1-65535).
2955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    server_port: Server port to forward the packets to (integer 1-65535).
2965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
2975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Preroute rules for accessing the port through external connections.
2985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING', '-i',
2995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
3005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '--to-port', server_port]
3015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error adding iptables rule for port %d.' % port)
3025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Output rules for accessing the rule through localhost or 127.0.0.1
3045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp',
3055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
3065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error adding iptables rule for port %d.' % port)
3075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _DeleteIptableRule(interface, port, server_port):
3105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Deletes the iptable rule associated with specified port number.
3115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
3135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    interface: Interface name to attach the filter to (string).
3145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    port: Port of incoming packets (integer 1-65535).
3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    server_port: Server port packets are forwarded to (integer 1-65535).
3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
3175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING', '-i',
3185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
3195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '--to-port', server_port]
3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error deleting iptables rule for port %d.' % port)
3215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp',
3235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
3245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error adding iptables rule for port %d.' % port)
3255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _DeleteAllIpTableRules():
3285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Deletes all iptables rules."""
3295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  command = ['sudo', 'iptables', '-t', 'nat', '-F']
3305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  _Exec(command, msg='Error deleting all iptables rules.')
3315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _Exec(command, msg=None):
3345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Executes a command.
3355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Args:
3375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    command: Command list to execute.
3385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    msg: Message describing the error in case the command fails.
3395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Returns:
3415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    The standard output from running the command.
3425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Raises:
3445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    TrafficControlError: If command fails. Message is set by the msg parameter.
3455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
3465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  cmd_list = [str(x) for x in command]
3475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  cmd = ' '.join(cmd_list)
3485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  logging.debug('Running command: %s', cmd)
3495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
3515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  output, error = p.communicate()
3525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if p.returncode != 0:
3535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    raise TrafficControlError(msg, cmd, p.returncode, output, error)
3545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return output.strip()
355