Snort integrate: Add the snort lib supporting snort integration. Add an sample application simple_switch_snort.py which can dump alert message.
When there is a Snort alert message, Ryu will receive an event called EventAlert. You can easily define the event handler in the method which using ‘set_ev_cls’ decorator with snortlib.EventAlert parameter. The simple_switch_snort.py can install a flow that mirroring incoming packets to the snort's NIC which correspond the OpenFlow switch on port 3 (by default). There are two methods that sending alert message to Ryu. 1. Ryu and Snort are both on the same machine. Ryu receives alert message via Unix Domain Socket. 2. Ryu and Snort are separate on different machines. Ryu receives alert message via Network Socket. More detail see doc/snort_integrate.rst Signed-off-by: Che-Wei Lin <linton.tw@gmail.com> Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
parent
86550bf898
commit
bbb7724423
152
doc/source/snort_integrate.rst
Normal file
152
doc/source/snort_integrate.rst
Normal file
@ -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)
|
145
ryu/app/simple_switch_snort.py
Normal file
145
ryu/app/simple_switch_snort.py
Normal file
@ -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)
|
125
ryu/lib/alert.py
Normal file
125
ryu/lib/alert.py
Normal file
@ -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
|
107
ryu/lib/snortlib.py
Normal file
107
ryu/lib/snortlib.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user