obfsucator / obfuscator.py
obfuscator.py
Raw
from ryu.base.app_manager import RyuApp
from ryu.controller import ofp_event
from ryu.controller.handler import HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER, set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet.ethernet import ethernet
from ryu.lib.packet.ipv4 import ipv4
from ryu.lib.packet.tcp import tcp
from ryu.lib.packet.udp import udp
from ryu.lib.ip import valid_ipv4
from ryu.lib.dpid import dpid_to_str

from netaddr import valid_mac

import json
import toml
import time


class Obfuscator(RyuApp):

    OFP_VERSIONS = [
        ofproto_v1_3.OFP_VERSION
    ]

    FIREWALL_TABLE = 0
    OBFUSCATOR_BYPASS_TABLE = 1
    OBFUSCATOR_DYNAMIC_TABLE = 2
    OBFUSCATOR_STATIC_TABLE = 3
    SWITCH_TABLE = 4

    TABLE_BASE_PRIORITY = 0
    HONEYPOT_BLOCK_PRIORITY = 256

    def __init__(self, *args, **kwargs):
        super(Obfuscator, self).__init__(*args, **kwargs)
        self.config = self.__load_configuration_file()
        print(self.config)
        self.switch_mappings = {}

    @ set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def features_handler(self, ev):
        '''
        Handshake: Features Request Response Handler
        '''
        datapath = ev.msg.datapath
        datapath_id = dpid_to_str(datapath.id)
        self.__clear_tables(datapath)
        self.__init_firewall_table(datapath)
        self.__init_obfuscator_tables(datapath)
        self.__init_switch_table(datapath)

        for rule in self.config.get(datapath_id, {}).get("firewall_rules", []):
            self.__install_firewall_rule(datapath, rule)
            self.logger.debug("๐Ÿ”ฅ\tinstalled firewall rule | {} | {}".format(rule.get("name", "unknown"), datapath_id))

        obfuscator_hidden_nodes = self.config.get(datapath_id, {}).get("hidden_node", [])
        obfuscator_rules = self.config.get(datapath_id, {}).get("rule", [])
        obfuscator_bypass_rules = self.config.get(datapath_id, {}).get("bypass", [])
        self.__obfuscator_install_bypass(datapath, obfuscator_hidden_nodes, obfuscator_bypass_rules)
        self.__obfuscator_install_static(datapath, obfuscator_hidden_nodes, obfuscator_rules)
        self.__obfuscator_block_honeypots(datapath, obfuscator_rules)

    @ set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def packet_in_handler(self, ev):

        datapath = ev.msg.datapath
        datapath_id = dpid_to_str(datapath.id)
        pkt = packet.Packet(ev.msg.data)
        in_port = ev.msg.match['in_port']
        self.__switch_learn(datapath, pkt, in_port, install=False)

        if ev.msg.table_id == self.OBFUSCATOR_STATIC_TABLE:
            # dynamic obfuscator logic
            self.logger.debug("โ†ฉ๏ธ\tinstalling reverse obfuscator mapping | {}".format(ev.msg.cookie))
            self.__obfuscator_flow_manager(datapath, pkt, ev.msg.cookie, in_port)
        else:
            # fallback l2 switching logic
            out_port = self.__switch_output_port(datapath, pkt, install=True)
            actions = [datapath.ofproto_parser.OFPActionOutput(out_port)]
            pkt_out_msg = datapath.ofproto_parser.OFPPacketOut(datapath=datapath, buffer_id=ev.msg.buffer_id, in_port=in_port, actions=actions, data=ev.msg.data)
            datapath.send_msg(pkt_out_msg)
            self.logger.debug("โžก๏ธ\tsent packet out via controller | {}".format(datapath_id))
        return

    # Init Functions

    def __clear_tables(self, datapath, table_id=-1):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        table = ofp.OFPTT_ALL if table_id < 0 else table_id
        mod = ofp_parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=table,
                                    command=ofp.OFPFC_DELETE,
                                    out_port=ofp.OFPP_ANY,
                                    out_group=ofp.OFPG_ANY
                                    )
        datapath.send_msg(mod)

    def __init_firewall_table(self, datapath):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        match = datapath.ofproto_parser.OFPMatch()
        instructions = [ofp_parser.OFPInstructionGotoTable(self.OBFUSCATOR_BYPASS_TABLE)]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.FIREWALL_TABLE, ofp.OFPFC_ADD,
                                    0, 0, self.TABLE_BASE_PRIORITY, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM,
                                    match, instructions)
        datapath.send_msg(mod)

    def __init_obfuscator_tables(self, datapath):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        match = datapath.ofproto_parser.OFPMatch()
        instructions = [ofp_parser.OFPInstructionGotoTable(self.OBFUSCATOR_DYNAMIC_TABLE)]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.OBFUSCATOR_BYPASS_TABLE, ofp.OFPFC_ADD,
                                    0, 0, self.TABLE_BASE_PRIORITY, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM,
                                    match, instructions)
        datapath.send_msg(mod)
        instructions = [ofp_parser.OFPInstructionGotoTable(self.OBFUSCATOR_STATIC_TABLE)]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.OBFUSCATOR_DYNAMIC_TABLE, ofp.OFPFC_ADD,
                                    0, 0, self.TABLE_BASE_PRIORITY, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM,
                                    match, instructions)
        datapath.send_msg(mod)
        instructions = [ofp_parser.OFPInstructionGotoTable(self.SWITCH_TABLE)]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.OBFUSCATOR_STATIC_TABLE, ofp.OFPFC_ADD,
                                    0, 0, self.TABLE_BASE_PRIORITY, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM,
                                    match, instructions)
        datapath.send_msg(mod)

    def __init_switch_table(self, datapath):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        match = datapath.ofproto_parser.OFPMatch()
        instructions = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [datapath.ofproto_parser.OFPActionOutput(
            datapath.ofproto.OFPP_CONTROLLER, datapath.ofproto.OFPCML_NO_BUFFER)])]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.SWITCH_TABLE, ofp.OFPFC_ADD,
                                    0, 0, self.TABLE_BASE_PRIORITY, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM,
                                    match, instructions)
        datapath.send_msg(mod)

    # Static Rule Functions

    def __install_firewall_rule(self, datapath, rule):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        if rule.get("block", True):
            instructions = [ofp_parser.OFPInstructionActions(ofp.OFPIT_CLEAR_ACTIONS, [])]
        else:
            instructions = [ofp_parser.OFPInstructionGotoTable(self.OBFUSCATOR_BYPASS_TABLE)]
        match = datapath.ofproto_parser.OFPMatch(**rule.get("match", {}))
        instructions = [ofp_parser.OFPInstructionActions(ofp.OFPIT_CLEAR_ACTIONS, [])]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.FIREWALL_TABLE, ofp.OFPFC_ADD,
                                    0, 0, rule.get("priority", self.TABLE_BASE_PRIORITY + 1), ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM | ofp.OFPFF_RESET_COUNTS,
                                    match, instructions)
        datapath.send_msg(mod)

    def __obfuscator_block_honeypots(self, datapath, rules):
        for rule in rules:
            fw_rule = {
                "name": "honeypot block",
                "block": True,
                "priority": self.HONEYPOT_BLOCK_PRIORITY,
                "match": {
                    "eth_dst": rule.get("honeypot", {}).get("mac_address"),
                    "eth_type": 2048,
                    "ipv4_dst": rule.get("honeypot", {}).get("ipv4_address")
                }
            }
            self.__install_firewall_rule(datapath, fw_rule)

    def __obfuscator_install_bypass(self, datapath, hidden_nodes, rules):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        instructions = [ofp_parser.OFPInstructionGotoTable(self.SWITCH_TABLE)]
        for node in hidden_nodes:
            for rule in rules:
                rule_match = rule
                rule_match["eth_dst"] = node.get("mac_address")
                rule_match["ipv4_dst"] = node.get("ipv4_address")
                rule_match["eth_type"] = 2048
                match = datapath.ofproto_parser.OFPMatch(**rule_match)
                mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                            self.OBFUSCATOR_BYPASS_TABLE, ofp.OFPFC_ADD,
                                            0, 0,  self.TABLE_BASE_PRIORITY + 1, ofp.OFP_NO_BUFFER,
                                            ofp.OFPP_ANY, ofp.OFPG_ANY,
                                            ofp.OFPFF_SEND_FLOW_REM | ofp.OFPFF_RESET_COUNTS,
                                            match, instructions)
                datapath.send_msg(mod)

    def __obfuscator_install_static(self, datapath, hidden_nodes, rules):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        instructions = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [datapath.ofproto_parser.OFPActionOutput(
            datapath.ofproto.OFPP_CONTROLLER, datapath.ofproto.OFPCML_NO_BUFFER)])]
        for node in hidden_nodes:
            for rule in rules:
                rule_match = rule.get("match", {})
                rule_match["eth_dst"] = node.get("mac_address")
                rule_match["ipv4_dst"] = node.get("ipv4_address")
                rule_match["eth_type"] = 2048
                match = datapath.ofproto_parser.OFPMatch(**rule_match)
                mod = ofp_parser.OFPFlowMod(datapath, rule.get("id"), 0,
                                            self.OBFUSCATOR_STATIC_TABLE, ofp.OFPFC_ADD,
                                            0, 0,  rule.get("priority", self.TABLE_BASE_PRIORITY + 1), ofp.OFP_NO_BUFFER,
                                            ofp.OFPP_ANY, ofp.OFPG_ANY,
                                            ofp.OFPFF_SEND_FLOW_REM | ofp.OFPFF_RESET_COUNTS,
                                            match, instructions)
                datapath.send_msg(mod)

    # Dynamic Logic

    def __switch_learn(self, datapath, pkt, in_port, install=False):
        if eth_header := pkt.get_protocol(ethernet):
            self.switch_mappings.setdefault(dpid_to_str(datapath.id), {})
            self.switch_mappings[dpid_to_str(datapath.id)][eth_header.src] = in_port
            if install:
                self.__switch_install_rule(datapath, eth_header.src, in_port)

    def __switch_output_port(self, datapath, pkt, install=False):
        if eth_header := pkt.get_protocol(ethernet):
            out_port = self.switch_mappings.get(dpid_to_str(datapath.id), {}).get(eth_header.dst, None) or datapath.ofproto.OFPP_FLOOD
            if install and not out_port == datapath.ofproto.OFPP_FLOOD:
                self.__switch_install_rule(datapath, eth_header.dst, out_port)
            return out_port
        return datapath.ofproto.OFPP_FLOOD

    def __switch_output_port_ethdst(self, datapath, eth_dst):
        out_port = self.switch_mappings.get(dpid_to_str(datapath.id), {}).get(eth_dst, None) or datapath.ofproto.OFPP_FLOOD
        return out_port

    def __switch_install_rule(self, datapath, dst_mac, output_port):
        ofp_parser = datapath.ofproto_parser
        ofp = datapath.ofproto
        match = datapath.ofproto_parser.OFPMatch(eth_dst=dst_mac)
        actions = [datapath.ofproto_parser.OFPActionOutput(output_port)]
        instructions = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = ofp_parser.OFPFlowMod(datapath, 0, 0,
                                    self.SWITCH_TABLE, ofp.OFPFC_ADD,
                                    60, 0,
                                    1, ofp.OFP_NO_BUFFER,
                                    ofp.OFPP_ANY, ofp.OFPG_ANY,
                                    ofp.OFPFF_SEND_FLOW_REM | ofp.OFPFF_RESET_COUNTS,
                                    match, instructions)
        datapath.send_msg(mod)

    def __obfuscator_flow_manager(self, datapath, pkt, rule_id, in_port):
        datapath_id = dpid_to_str(datapath.id)
        rules = self.config.get(datapath_id, {}).get("rule", [])
        matched_rule = None
        for rule in rules:
            if rule.get("id") == rule_id:
                matched_rule = rule
        if not matched_rule:
            self.logger.error("๐Ÿ†˜\tfailed to find a matching obfuscator rule")
            return

        # 1. create a 5 tuple of the packet as is
        ft = {"eth_type": 2048}
        if eth_header := pkt.get_protocol(ethernet):
            ft["eth_src"] = eth_header.src
            ft["eth_dst"] = eth_header.dst
        if ipv4_header := pkt.get_protocol(ipv4):
            ft["ipv4_src"] = ipv4_header.src
            ft["ipv4_dst"] = ipv4_header.dst
            ft["ip_proto"] = ipv4_header.proto
        else:
            self.logger.error("๐Ÿค”\tnon-ipv4 packet will not be obfuscated")
            return
        if tcp_header := pkt.get_protocol(tcp):
            ft["tcp_src"] = tcp_header.src_port
            ft["tcp_dst"] = tcp_header.dst_port
        elif udp_header := pkt.get_protocol(udp):
            ft["udp_src"] = udp_header.src_port
            ft["udp_dst"] = udp_header.dst_port
        else:
            self.logger.error("๐Ÿค”\tnon-tcp/udp packet will not be obfuscated")
            return

        # 2. actions are to change the dst ipv4 and mac and output to the
        #    honeypot's port (or flood)
        actions = [
            datapath.ofproto_parser.OFPActionSetField(ipv4_dst=matched_rule.get("honeypot", {}).get("ipv4_address")),
            datapath.ofproto_parser.OFPActionSetField(eth_dst=matched_rule.get("honeypot", {}).get("mac_address")),
            datapath.ofproto_parser.OFPActionOutput(self.__switch_output_port_ethdst(datapath, matched_rule.get("honeypot", {}).get("mac_address")))
        ]

        # 3. Send the packet out and install the relevant flow mod (with an idle
        #    timeout)
        pkt_out_msg = datapath.ofproto_parser.OFPPacketOut(datapath=datapath, buffer_id=datapath.ofproto.OFP_NO_BUFFER, in_port=in_port, actions=actions, data=pkt.serialize())
        datapath.send_msg(pkt_out_msg)
        inst = [datapath.ofproto_parser.OFPInstructionActions(datapath.ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = datapath.ofproto_parser.OFPFlowMod(
            datapath=datapath, priority=2, match=datapath.ofproto_parser.OFPMatch(**ft), instructions=inst, idle_timeout=120, hard_timeout=0, table_id=self.OBFUSCATOR_DYNAMIC_TABLE)
        self.logger.info("โœ๏ธ\tflow-Mod written to datapath: {}".format(dpid_to_str(datapath.id)))
        datapath.send_msg(mod)

        # ----
        # 1. create a 5 tuple of the expected return packet
        reverse_ft = {"eth_type": 2048}
        if ipv4_header := pkt.get_protocol(ipv4):
            reverse_ft["ipv4_src"] = matched_rule.get("honeypot", {}).get("ipv4_address")
            reverse_ft["ipv4_dst"] = ipv4_header.src
            reverse_ft["ip_proto"] = ipv4_header.proto
        else:
            self.logger.error("๐Ÿค”\tnon-ipv4 packet will not be obfuscated")
            return
        if tcp_header := pkt.get_protocol(tcp):
            reverse_ft["tcp_src"] = tcp_header.dst_port
            reverse_ft["tcp_dst"] = tcp_header.src_port
        elif udp_header := pkt.get_protocol(udp):
            reverse_ft["udp_src"] = udp_header.dst_port
            reverse_ft["udp_dst"] = udp_header.src_port
        else:
            self.logger.error("๐Ÿค”\tnon-tcp/udp packet will not be obfuscated")
            return
        # 2. actions are to change the src ipv4 and mac and output to the
        #    original target's port (or flood)
        actions = [
            datapath.ofproto_parser.OFPActionSetField(ipv4_src=ft.get("ipv4_dst")),
            datapath.ofproto_parser.OFPActionSetField(eth_src=ft.get("eth_dst")),
            datapath.ofproto_parser.OFPActionOutput(in_port)
        ]
        # 3. install the relevant flow mod (with an idle
        #    timeout)
        inst = [datapath.ofproto_parser.OFPInstructionActions(datapath.ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = datapath.ofproto_parser.OFPFlowMod(datapath=datapath, priority=3, match=datapath.ofproto_parser.OFPMatch(**
                                                 reverse_ft), instructions=inst, idle_timeout=120, hard_timeout=0, table_id=self.OBFUSCATOR_DYNAMIC_TABLE)
        self.logger.info("โœ๏ธ\tflow-Mod written to datapath: {}".format(dpid_to_str(datapath.id)))
        datapath.send_msg(mod)

    # Config

    def __load_configuration_file(self, file_path="/tmp/config.toml"):
        try:
            with open(file_path) as toml_file:
                self.logger.debug("๐Ÿ“š\ttoml config file opened: {}".format(file_path))
                config_data = toml.load(toml_file)
                self.logger.debug("โš™๏ธ\tparsed config toml file as dictionary: {}".format(file_path))
                return config_data
        except Exception as e:
            self.logger.error("๐Ÿ†˜\tfailed to load or parse toml from file: {}".format(file_path))
        return {}