1
0

main.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. interfaces = get_interfaces()
  61. return render_template(
  62. "main.html",
  63. rules=rules,
  64. units=BANDWIDTH_UNITS,
  65. standard_unit=STANDARD_UNIT,
  66. interfaces=interfaces,
  67. )
  68. @app.route("/new_rule/<interface>", methods=["POST"])
  69. def new_rule(interface):
  70. delay = request.form["Delay"]
  71. delay_variance = request.form["DelayVariance"]
  72. loss = request.form["Loss"]
  73. loss_correlation = request.form["LossCorrelation"]
  74. duplicate = request.form["Duplicate"]
  75. reorder = request.form["Reorder"]
  76. reorder_correlation = request.form["ReorderCorrelation"]
  77. corrupt = request.form["Corrupt"]
  78. limit = request.form["Limit"]
  79. rate = request.form["Rate"]
  80. rate_unit = request.form["rate_unit"]
  81. interface = filter_interface_name(interface)
  82. # remove old setup
  83. command = f"tc qdisc del dev {interface} root netem"
  84. command = command.split(" ")
  85. proc = subprocess.Popen(command)
  86. proc.wait()
  87. # apply new setup
  88. command = f"tc qdisc add dev {interface} root netem"
  89. if rate != "":
  90. command += f" rate {rate}{rate_unit}"
  91. if delay != "":
  92. command += f" delay {delay}ms"
  93. if delay_variance != "":
  94. command += f" {delay_variance}ms"
  95. if loss != "":
  96. command += f" loss {loss}%"
  97. if loss_correlation != "":
  98. command += f" {loss_correlation}%"
  99. if duplicate != "":
  100. command += f" duplicate {duplicate}%"
  101. if reorder != "":
  102. command += f" reorder {reorder}%"
  103. if reorder_correlation != "":
  104. command += f" {reorder_correlation}%"
  105. if corrupt != "":
  106. command += f" corrupt {corrupt}%"
  107. if limit != "":
  108. command += f" limit {limit}"
  109. print(command)
  110. command = command.split(" ")
  111. proc = subprocess.Popen(command)
  112. proc.wait()
  113. return redirect(url_for("main") + "#" + interface)
  114. @app.route("/remove_rule/<interface>", methods=["POST"])
  115. def remove_rule(interface):
  116. interface = filter_interface_name(interface)
  117. # remove old setup
  118. command = f"tc qdisc del dev {interface} root netem"
  119. command = command.split(" ")
  120. proc = subprocess.Popen(command)
  121. proc.wait()
  122. return redirect(url_for("main") + "#" + interface)
  123. def filter_interface_name(interface):
  124. return re.sub(r"[^A-Za-z0-9_-]+", "", interface)
  125. def run_ip_command(command_args):
  126. """Runs the 'ip' command with the specified arguments and returns the output."""
  127. try:
  128. result = subprocess.run(
  129. command_args,
  130. capture_output=True,
  131. text=True,
  132. check=True,
  133. )
  134. return result.stdout
  135. except subprocess.CalledProcessError as e:
  136. print(f"Error executing command '{command_args}': {e}")
  137. return ""
  138. def get_active_rules():
  139. proc = subprocess.Popen(["tc", "qdisc"], stdout=subprocess.PIPE)
  140. output = proc.communicate()[0].decode()
  141. lines = output.split("\n")[:-1]
  142. rules = []
  143. dev = set()
  144. for line in lines:
  145. arguments = line.split()
  146. rule = parse_rule(arguments)
  147. if rule["name"] and rule["name"] not in dev:
  148. rule["ip"] = get_interface_ip(rule["name"])
  149. rules.append(rule)
  150. dev.add(rule["name"])
  151. rules.sort(key=lambda x: x["name"])
  152. return rules
  153. def get_interfaces():
  154. output = run_ip_command(["ip", "-o", "-4", "addr", "show"])
  155. interfaces = {}
  156. for line in output.split("\n"):
  157. if line:
  158. parts = line.split()
  159. iface = parts[1]
  160. ip = parts[3].split("/")[0]
  161. interfaces[iface] = ip
  162. return interfaces
  163. def get_interface_ip(interface):
  164. output = run_ip_command(["ip", "-o", "-4", "addr", "show", interface])
  165. match = re.search(r"inet (\d+\.\d+\.\d+\.\d+)", output)
  166. return match.group(1) if match else "No IP found"
  167. def parse_rule(split_rule):
  168. # pylint: disable=too-many-branches
  169. rule = {
  170. "name": None,
  171. "ip": None,
  172. "rate": None,
  173. "delay": None,
  174. "delayVariance": None,
  175. "loss": None,
  176. "lossCorrelation": None,
  177. "duplicate": None,
  178. "reorder": None,
  179. "reorderCorrelation": None,
  180. "corrupt": None,
  181. "limit": None,
  182. }
  183. i = 0
  184. for argument in split_rule:
  185. if argument == "dev":
  186. # Both regex PATTERN and dev name can be given
  187. # An interface could match the PATTERN and/or
  188. # be in the interface list
  189. if PATTERN is None and DEV_LIST is None:
  190. rule["name"] = split_rule[i + 1]
  191. if PATTERN:
  192. if PATTERN.match(split_rule[i + 1]):
  193. rule["name"] = split_rule[i + 1]
  194. if DEV_LIST:
  195. if split_rule[i + 1] in DEV_LIST:
  196. rule["name"] = split_rule[i + 1]
  197. elif argument == "rate":
  198. rule["rate"] = split_rule[i + 1].split("Mbit")[0]
  199. elif argument == "delay":
  200. rule["delay"] = split_rule[i + 1]
  201. if len(split_rule) > (i + 2) and "ms" in split_rule[i + 2]:
  202. rule["delayVariance"] = split_rule[i + 2]
  203. elif argument == "loss":
  204. rule["loss"] = split_rule[i + 1]
  205. if len(split_rule) > (i + 2) and "%" in split_rule[i + 2]:
  206. rule["lossCorrelation"] = split_rule[i + 2]
  207. elif argument == "duplicate":
  208. rule["duplicate"] = split_rule[i + 1]
  209. elif argument == "reorder":
  210. rule["reorder"] = split_rule[i + 1]
  211. if len(split_rule) > (i + 2) and "%" in split_rule[i + 2]:
  212. rule["reorderCorrelation"] = split_rule[i + 2]
  213. elif argument == "corrupt":
  214. rule["corrupt"] = split_rule[i + 1]
  215. elif argument == "limit":
  216. rule["limit"] = split_rule[i + 1]
  217. i += 1
  218. return rule
  219. if __name__ == "__main__":
  220. if os.geteuid() != 0:
  221. print(
  222. "You need to have root privileges to run this script.\n"
  223. "Please try again, this time using 'sudo'. Exiting."
  224. )
  225. sys.exit(1)
  226. # TC Variables
  227. args = parse_arguments()
  228. PATTERN = re.compile(args.regex) if args.regex else args.regex
  229. DEV_LIST = args.dev
  230. # Flask Variable
  231. app_args = {"host": args.ip, "port": args.port}
  232. if not args.debug:
  233. app_args["debug"] = False
  234. app.debug = True
  235. app.run(**app_args)