diff options
-rw-r--r-- | doc/source/snort_integrate.rst | 152 | ||||
-rw-r--r-- | ryu/app/simple_switch_snort.py | 145 | ||||
-rw-r--r-- | ryu/lib/alert.py | 125 | ||||
-rw-r--r-- | ryu/lib/snortlib.py | 107 |
4 files changed, 529 insertions, 0 deletions
diff --git a/doc/source/snort_integrate.rst b/doc/source/snort_integrate.rst new file mode 100644 index 00000000..f6b593d7 --- /dev/null +++ b/doc/source/snort_integrate.rst @@ -0,0 +1,152 @@ +****************** +Snort Intergration +****************** + +This document describes how to integrate Ryu with Snort. + +Overview +==== + +**[Option 1] Ryu and Snort are on the same machine** +:: + + +---------------------+ + | unixsock | + | Ryu == snort | + +----eth0-----eth1----+ + | | + +-------+ +----------+ +-------+ + | HostA |---| OFSwitch |---| HostB | + +-------+ +----------+ +-------+ + + +The above depicts Ryu and Snort architecture. Ryu receives Snort alert packet via **Unix Domain Socket** . To monitor packets between HostA and HostB, installing a flow that mirrors packets to Snort. + + +**[Option 2] Ryu and Snort are on the different machines** +:: + + +---------------+ + | Snort eth0--| + | Sniffer | | + +-----eth1------+ | + | | + +-------+ +----------+ +-----------+ + | HostA |---| OFSwitch |---| LAN (*CP) | + +-------+ +----------+ +-----------+ + | | + +----------+ +----------+ + | HostB | | Ryu | + +----------+ +----------+ + + +**\*CP: Controller Plane** + +The above depicts Ryu and Snort architecture. Ryu receives Snort alert packet via **Network Socket** . To monitor packets between HostA and HostB, installing a flow that mirrors packets to Snort. + + + +Installation Snort +==== +Snort is an open source network intrusion prevention and detectionsystem developed by Sourcefire. If you are not familiar with installing/setting up Snort, please referto snort setup guides. + +http://www.snort.org/docs + + + +Configure Snort +==== +The configuration example is below: + +- Add a snort rules file into ``/etc/snort/rules`` named ``Myrules.rules`` :: + + alert icmp any any -> any any (msg:"Pinging...";sid:1000004;) + alert tcp any any -> any 80 (msg:"Port 80 is accessing"; sid:1000003;) + +- Add the custom rules in ``/etc/snort/snort.conf`` :: + + include $RULE_PATH/Myrules.rules + +Configure NIC as a promiscuous mode. :: + + $ sudo ifconfig eth1 promisc + + +Usage +==== +**[Option 1]** + +1. Modify the ``simple_switch_snort.py``: :: + + socket_config = {'unixsock': True} + # True: Unix Domain Socket Server [Option1] + # False: Network Socket Server [Option2] + + +2. Run Ryu with sample application: :: + + $ sudo ./bin/ryu-manager ryu/app/simple_switch_snort.py + +The incoming packets will all mirror to **port 3** which should be connect to Snort network interface. You can modify the mirror port by assign a new value in the ``self.snort_port = 3`` of ``simple_switch_snort.py`` + +3. Run Snort: :: + + $ sudo -i + $ sudo snort -i eth1 -A unsock -l /tmp -c /etc/snort/snort.conf + +4. Send an ICMP packet from HostA (192.168.8.40) to HostB (192.168.8.50): :: + + $ ping 192.168.8.50 + +5. You can see the result under next section. + + +===== + +**[Option 2]** + +1. Modify the ``simple_switch_snort.py``: :: + + socket_config = {'unixsock': False} + # True: Unix Domain Socket Server [Option1] + # False: Network Socket Server [Option2] + + +2. Run Ryu with sample application (On the Controller): :: + + $ sudo ./bin/ryu-manager ryu/app/simple_switch_snort.py + +3. Run Snort (On the Snort machine): :: + + $ sudo -i + $ sudo snort -i eth1 -A unsock -l /tmp -c /etc/snort/snort.conf + +4. Run ``unsock2nwsock.py`` (On the Snort machine): :: + + $ sudo python unsock2nwsock.py + +This program listening snort alert messages from unix domain socket and sending it to Ryu using network socket. + +You can clone the script over here. https://gist.github.com/John-Lin/9408ab716df57dbe32ca + + +5. Send an ICMP packet from HostA (192.168.8.40) to HostB (192.168.8.50): :: + + $ ping 192.168.8.50 + + +6. You can see the alert message below: :: + + + alertmsg: Pinging... + icmp(code=0,csum=19725,data=echo(data=array('B', [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 97, 98, 99, 100, 101, 102, 103, 104, 105]),id=1,seq=78),type=8) + + ipv4(csum=42562,dst='192.168.8.50',flags=0,header_length=5,identification=724,offset=0,option=None,proto=1,src='192.168.8.40',tos=0,total_length=60,ttl=128,version=4) + + ethernet(dst='00:23:54:5a:05:14',ethertype=2048,src='00:23:54:6c:1d:17') + + + alertmsg: Pinging... + icmp(code=0,csum=21773,data=echo(data=array('B', [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 97, 98, 99, 100, 101, 102, 103, 104, 105]),id=1,seq=78),type=0) + + ipv4(csum=52095,dst='192.168.8.40',flags=0,header_length=5,identification=7575,offset=0,option=None,proto=1,src='192.168.8.50',tos=0,total_length=60,ttl=64,version=4) diff --git a/ryu/app/simple_switch_snort.py b/ryu/app/simple_switch_snort.py new file mode 100644 index 00000000..51c3d1e0 --- /dev/null +++ b/ryu/app/simple_switch_snort.py @@ -0,0 +1,145 @@ +# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import array + +from ryu.base import app_manager +from ryu.controller import ofp_event +from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER +from ryu.controller.handler import set_ev_cls +from ryu.ofproto import ofproto_v1_3 +from ryu.lib.packet import packet +from ryu.lib.packet import ethernet +from ryu.lib.packet import ipv4 +from ryu.lib.packet import icmp +from ryu.lib import snortlib + + +class SimpleSwitchSnort(app_manager.RyuApp): + OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] + _CONTEXTS = {'snortlib': snortlib.SnortLib} + + def __init__(self, *args, **kwargs): + super(SimpleSwitchSnort, self).__init__(*args, **kwargs) + self.snort = kwargs['snortlib'] + self.snort_port = 3 + self.mac_to_port = {} + + socket_config = {'unixsock': True} + + self.snort.set_config(socket_config) + self.snort.start_socket_server() + + def packet_print(self, pkt): + pkt = packet.Packet(array.array('B', pkt)) + + eth = pkt.get_protocol(ethernet.ethernet) + _ipv4 = pkt.get_protocol(ipv4.ipv4) + _icmp = pkt.get_protocol(icmp.icmp) + + if _icmp: + self.logger.info("%r", _icmp) + + if _ipv4: + self.logger.info("%r", _ipv4) + + if eth: + self.logger.info("%r", eth) + + # for p in pkt.protocols: + # if hasattr(p, 'protocol_name') is False: + # break + # print 'p:', p.protocol_name + + @set_ev_cls(snortlib.EventAlert, MAIN_DISPATCHER) + def _dump_alert(self, ev): + msg = ev.msg + + print 'alertmsg:', ''.join(msg.alertmsg) + + self.packet_print(msg.pkt) + + @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) + def switch_features_handler(self, ev): + datapath = ev.msg.datapath + ofproto = datapath.ofproto + parser = datapath.ofproto_parser + + # install table-miss flow entry + # + # We specify NO BUFFER to max_len of the output action due to + # OVS bug. At this moment, if we specify a lesser number, e.g., + # 128, OVS will send Packet-In with invalid buffer_id and + # truncated packet data. In that case, we cannot output packets + # correctly. + match = parser.OFPMatch() + actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, + ofproto.OFPCML_NO_BUFFER)] + self.add_flow(datapath, 0, match, actions) + + def add_flow(self, datapath, priority, match, actions): + ofproto = datapath.ofproto + parser = datapath.ofproto_parser + + inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, + actions)] + + mod = parser.OFPFlowMod(datapath=datapath, priority=priority, + match=match, instructions=inst) + datapath.send_msg(mod) + + @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) + def _packet_in_handler(self, ev): + msg = ev.msg + datapath = msg.datapath + ofproto = datapath.ofproto + parser = datapath.ofproto_parser + in_port = msg.match['in_port'] + + pkt = packet.Packet(msg.data) + eth = pkt.get_protocols(ethernet.ethernet)[0] + + dst = eth.dst + src = eth.src + + dpid = datapath.id + self.mac_to_port.setdefault(dpid, {}) + + # self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port) + + # learn a mac address to avoid FLOOD next time. + self.mac_to_port[dpid][src] = in_port + + if dst in self.mac_to_port[dpid]: + out_port = self.mac_to_port[dpid][dst] + else: + out_port = ofproto.OFPP_FLOOD + + actions = [parser.OFPActionOutput(out_port), + parser.OFPActionOutput(self.snort_port)] + + # install a flow to avoid packet_in next time + if out_port != ofproto.OFPP_FLOOD: + match = parser.OFPMatch(in_port=in_port, eth_dst=dst) + self.add_flow(datapath, 1, match, actions) + + data = None + if msg.buffer_id == ofproto.OFP_NO_BUFFER: + data = msg.data + + out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id, + in_port=in_port, actions=actions, data=data) + datapath.send_msg(out) diff --git a/ryu/lib/alert.py b/ryu/lib/alert.py new file mode 100644 index 00000000..5095bc37 --- /dev/null +++ b/ryu/lib/alert.py @@ -0,0 +1,125 @@ +# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import struct +from struct import calcsize + + +class SfTimeval32(object): + _PACK_STR = '!II' + _SIZE = 8 + + def __init__(self, tv_sec, tv_usec): + self.tv_sec = tv_sec + self.tv_usec = tv_usec + + @classmethod + def parser(cls, buf, offset): + (tv_sec, tv_usec) = struct.unpack_from( + cls._PACK_STR, buf, offset) + + msg = cls(tv_sec, tv_usec) + + return msg + + +class Event(object): + _PACK_STR = '!IIIIIII' + _SIZE = 36 + + def __init__(self, sig_generator, sig_id, sig_rev, classification, + priority, event_id, event_reference, ref_time): + self.sig_generator = sig_generator + self.sig_id = sig_id + self.sig_rev = sig_rev + self.classification = classification + self.priority = priority + self.event_id = event_id + self.event_reference = event_reference + self.ref_time = ref_time + + @classmethod + def parser(cls, buf, offset): + (sig_generator, sig_id, sig_rev, classification, priority, + event_id, event_reference) = struct.unpack_from( + cls._PACK_STR, buf, offset) + offset += calcsize(cls._PACK_STR) + + ref_time = SfTimeval32.parser(buf, offset) + + msg = cls(sig_generator, sig_id, sig_rev, classification, + priority, event_id, event_reference, ref_time) + + return msg + + +class PcapPktHdr32(object): + _PACK_STR = '!II' + _SIZE = 16 + + def __init__(self, ts, caplen, len_): + self.ts = ts + self.caplen = caplen + self.len = len_ + + @classmethod + def parser(cls, buf, offset): + ts = SfTimeval32.parser(buf, offset) + offset += SfTimeval32._SIZE + + (caplen, len_) = struct.unpack_from( + cls._PACK_STR, buf, offset) + + msg = cls(ts, caplen, len_) + + return msg + + +class AlertPkt(object): + _ALERTMSG_PACK_STR = '!256s' + _ALERTPKT_PART_PACK_STR = '!IIIII65535s' + _ALERTPKT_SIZE = 65863 + + def __init__(self, alertmsg, pkth, dlthdr, nethdr, transhdr, data, + val, pkt, event): + self.alertmsg = alertmsg + self.pkth = pkth + self.dlthdr = dlthdr + self.nethdr = nethdr + self.transhdr = transhdr + self.data = data + self.val = val + self.pkt = pkt + self.event = event + + @classmethod + def parser(cls, buf): + alertmsg = struct.unpack_from(cls._ALERTMSG_PACK_STR, buf) + offset = calcsize(cls._ALERTMSG_PACK_STR) + + pkth = PcapPktHdr32.parser(buf, offset) + offset += PcapPktHdr32._SIZE + + (dlthdr, nethdr, transhdr, data, val, pkt) = \ + struct.unpack_from(cls._ALERTPKT_PART_PACK_STR, buf, + offset) + offset += calcsize(cls._ALERTPKT_PART_PACK_STR) + + event = Event.parser(buf, offset) + + msg = cls(alertmsg, pkth, dlthdr, nethdr, transhdr, data, val, + pkt, event) + + return msg diff --git a/ryu/lib/snortlib.py b/ryu/lib/snortlib.py new file mode 100644 index 00000000..2773585f --- /dev/null +++ b/ryu/lib/snortlib.py @@ -0,0 +1,107 @@ +# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import logging + +from ryu.lib import hub +from ryu.base import app_manager +from ryu.controller import event +import alert + + +BUFSIZE = alert.AlertPkt._ALERTPKT_SIZE +SOCKFILE = "/tmp/snort_alert" + + +class EventAlert(event.EventBase): + def __init__(self, msg): + super(EventAlert, self).__init__() + self.msg = msg + + +class SnortLib(app_manager.RyuApp): + + def __init__(self): + super(SnortLib, self).__init__() + self.name = 'snortlib' + self.config = {'unixsock': True} + self._set_logger() + + def set_config(self, config): + assert isinstance(config, dict) + self.config = config + + def start_socket_server(self): + if not self.config.get('unixsock'): + self.config['ip'] = hub.socket.gethostbyname(hub.socket. + gethostname()) + if self.config.get('port') is None: + self.config['port'] = 51234 + + self._start_recv_nw_sock(self.config.get('ip'), + self.config.get('port')) + else: + self._start_recv() + + self.logger.info(self.config) + + def _recv_loop(self): + self.logger.info("Unix socket start listening...") + while True: + data = self.sock.recv(BUFSIZE) + msg = alert.AlertPkt.parser(data) + if msg: + self.send_event_to_observers(EventAlert(msg)) + + def _start_recv(self): + if os.path.exists(SOCKFILE): + os.unlink(SOCKFILE) + + self.sock = hub.socket.socket(hub.socket.AF_UNIX, + hub.socket.SOCK_DGRAM) + self.sock.bind(SOCKFILE) + hub.spawn(self._recv_loop) + + def _start_recv_nw_sock(self, ip, port): + + self.nwsock = hub.socket.socket(hub.socket.AF_INET, + hub.socket.SOCK_STREAM) + self.nwsock.bind((ip, port)) + self.nwsock.listen(5) + self.conn, addr = self.nwsock.accept() + + hub.spawn(self._recv_loop_nw_sock) + + def _recv_loop_nw_sock(self): + self.logger.info("Network socket server start listening...") + while True: + data = self.conn.recv(BUFSIZE, hub.socket.MSG_WAITALL) + + if len(data) == BUFSIZE: + msg = alert.AlertPkt.parser(data) + if msg: + self.send_event_to_observers(EventAlert(msg)) + else: + self.logger.debug(len(data)) + + def _set_logger(self): + """change log format.""" + self.logger.propagate = False + hdl = logging.StreamHandler() + fmt_str = '[snort][%(levelname)s] %(message)s' + hdl.setFormatter(logging.Formatter(fmt_str)) + self.logger.addHandler(hdl) |