summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--doc/source/snort_integrate.rst152
-rw-r--r--ryu/app/simple_switch_snort.py145
-rw-r--r--ryu/lib/alert.py125
-rw-r--r--ryu/lib/snortlib.py107
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)