main.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. """TC web GUI."""
  2. import argparse
  3. import os
  4. import re
  5. import subprocess
  6. import sys
  7. from flask import Flask, redirect, render_template, request, url_for
  8. BANDWIDTH_UNITS = [
  9. "bit", # Bits per second
  10. "kbit", # Kilobits per second
  11. "mbit", # Megabits per second
  12. "gbit", # Gigabits per second
  13. "tbit", # Terabits per second
  14. "bps", # Bytes per second
  15. "kbps", # Kilobytes per second
  16. "mbps", # Megabytes per second
  17. "gbps", # Gigabytes per second
  18. "tbps", # Terabytes per second
  19. ]
  20. STANDARD_UNIT = "mbit"
  21. app = Flask(__name__)
  22. PATTERN = None
  23. DEV_LIST = None
  24. app.static_folder = "static"
  25. def parse_arguments():
  26. parser = argparse.ArgumentParser(
  27. description=__doc__,
  28. formatter_class=argparse.ArgumentDefaultsHelpFormatter,
  29. )
  30. parser.add_argument(
  31. "--ip",
  32. type=str,
  33. default=os.environ.get("TCGUI_IP"),
  34. help="The IP where the server is listening",
  35. )
  36. parser.add_argument(
  37. "--port",
  38. type=int,
  39. default=os.environ.get("TCGUI_PORT"),
  40. help="The port where the server is listening",
  41. )
  42. parser.add_argument(
  43. "--dev",
  44. type=str,
  45. nargs="*",
  46. default=os.environ.get("TCGUI_DEV"),
  47. help="The interfaces to restrict to",
  48. )
  49. parser.add_argument(
  50. "--regex",
  51. type=str,
  52. default=os.environ.get("TCGUI_REGEX"),
  53. help="A regex to match interfaces",
  54. )
  55. parser.add_argument("--debug", action="store_true", help="Run Flask in debug mode")
  56. return parser.parse_args()
  57. @app.route("/")
  58. def main():
  59. rules = get_active_rules()
  60. return render_template(
  61. "main.html", rules=rules, units=BANDWIDTH_UNITS, standard_unit=STANDARD_UNIT
  62. )
  63. @app.route("/new_rule/<interface>", methods=["POST"])
  64. def new_rule(interface):
  65. delay = request.form["Delay"]
  66. delay_variance = request.form["DelayVariance"]
  67. loss = request.form["Loss"]
  68. loss_correlation = request.form["LossCorrelation"]
  69. duplicate = request.form["Duplicate"]
  70. reorder = request.form["Reorder"]
  71. reorder_correlation = request.form["ReorderCorrelation"]
  72. corrupt = request.form["Corrupt"]
  73. limit = request.form["Limit"]
  74. rate = request.form["Rate"]
  75. rate_unit = request.form["rate_unit"]
  76. interface = filter_interface_name(interface)
  77. # remove old setup
  78. command = f"tc qdisc del dev {interface} root netem"
  79. command = command.split(" ")
  80. proc = subprocess.Popen(command)
  81. proc.wait()
  82. # apply new setup
  83. command = f"tc qdisc add dev {interface} root netem"
  84. if rate != "":
  85. command += f" rate {rate}{rate_unit}"
  86. if delay != "":
  87. command += f" delay {delay}ms"
  88. if delay_variance != "":
  89. command += f" {delay_variance}ms"
  90. if loss != "":
  91. command += f" loss {loss}%%"
  92. if loss_correlation != "":
  93. command += f" {loss_correlation}%%"
  94. if duplicate != "":
  95. command += f" duplicate {duplicate}%%"
  96. if reorder != "":
  97. command += f" reorder {reorder}%%"
  98. if reorder_correlation != "":
  99. command += f" {reorder_correlation}%%"
  100. if corrupt != "":
  101. command += f" corrupt {corrupt}%%"
  102. if limit != "":
  103. command += f" limit {limit}"
  104. print(command)
  105. command = command.split(" ")
  106. proc = subprocess.Popen(command)
  107. proc.wait()
  108. return redirect(url_for("main") + "#" + interface)
  109. @app.route("/remove_rule/<interface>", methods=["POST"])
  110. def remove_rule(interface):
  111. interface = filter_interface_name(interface)
  112. # remove old setup
  113. command = f"tc qdisc del dev {interface} root netem"
  114. command = command.split(" ")
  115. proc = subprocess.Popen(command)
  116. proc.wait()
  117. return redirect(url_for("main") + "#" + interface)
  118. def filter_interface_name(interface):
  119. return re.sub(r"[^A-Za-z0-9_-]+", "", interface)
  120. def get_active_rules():
  121. proc = subprocess.Popen(["tc", "qdisc"], stdout=subprocess.PIPE)
  122. output = proc.communicate()[0].decode()
  123. lines = output.split("\n")[:-1]
  124. rules = []
  125. dev = set()
  126. for line in lines:
  127. arguments = line.split()
  128. rule = parse_rule(arguments)
  129. if rule["name"] and rule["name"] not in dev:
  130. rules.append(rule)
  131. dev.add(rule["name"])
  132. return rules
  133. def parse_rule(split_rule):
  134. # pylint: disable=too-many-branches
  135. rule = {
  136. "name": None,
  137. "rate": None,
  138. "delay": None,
  139. "delayVariance": None,
  140. "loss": None,
  141. "lossCorrelation": None,
  142. "duplicate": None,
  143. "reorder": None,
  144. "reorderCorrelation": None,
  145. "corrupt": None,
  146. "limit": None,
  147. }
  148. i = 0
  149. for argument in split_rule:
  150. if argument == "dev":
  151. # Both regex PATTERN and dev name can be given
  152. # An interface could match the PATTERN and/or
  153. # be in the interface list
  154. if PATTERN is None and DEV_LIST is None:
  155. rule["name"] = split_rule[i + 1]
  156. if PATTERN:
  157. if PATTERN.match(split_rule[i + 1]):
  158. rule["name"] = split_rule[i + 1]
  159. if DEV_LIST:
  160. if split_rule[i + 1] in DEV_LIST:
  161. rule["name"] = split_rule[i + 1]
  162. elif argument == "rate":
  163. rule["rate"] = split_rule[i + 1].split("Mbit")[0]
  164. elif argument == "delay":
  165. rule["delay"] = split_rule[i + 1]
  166. if len(split_rule) > (i + 2) and "ms" in split_rule[i + 2]:
  167. rule["delayVariance"] = split_rule[i + 2]
  168. elif argument == "loss":
  169. rule["loss"] = split_rule[i + 1]
  170. if len(split_rule) > (i + 2) and "%" in split_rule[i + 2]:
  171. rule["lossCorrelation"] = split_rule[i + 2]
  172. elif argument == "duplicate":
  173. rule["duplicate"] = split_rule[i + 1]
  174. elif argument == "reorder":
  175. rule["reorder"] = split_rule[i + 1]
  176. if len(split_rule) > (i + 2) and "%" in split_rule[i + 2]:
  177. rule["reorderCorrelation"] = split_rule[i + 2]
  178. elif argument == "corrupt":
  179. rule["corrupt"] = split_rule[i + 1]
  180. elif argument == "limit":
  181. rule["limit"] = split_rule[i + 1]
  182. i += 1
  183. return rule
  184. if __name__ == "__main__":
  185. if os.geteuid() != 0:
  186. print(
  187. "You need to have root privileges to run this script.\n"
  188. "Please try again, this time using 'sudo'. Exiting."
  189. )
  190. sys.exit(1)
  191. # TC Variables
  192. args = parse_arguments()
  193. PATTERN = re.compile(args.regex) if args.regex else args.regex
  194. DEV_LIST = args.dev
  195. # Flask Variable
  196. app_args = {"host": args.ip, "port": args.port}
  197. if not args.debug:
  198. app_args["debug"] = False
  199. app.debug = True
  200. app.run(**app_args)