├── .gitignore ├── CHANGES ├── COPYING ├── README ├── README.md ├── VERSION ├── acld ├── acld.py ├── broker-acld-rc.d.sh └── example.zeek ├── command-line ├── command-line.py ├── commands.yaml └── example.zeek ├── netcontrol ├── __init__.py └── api.py ├── openflow ├── controller.py └── example.zeek └── test ├── simple-client.py └── simple-test.zeek /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2 | 0.6 | 2020-11-26 17:59:05 +0000 3 | 4 | * Release 0.6. 5 | 6 | 0.5-2 | 2020-11-26 17:58:52 +0000 7 | 8 | * Change Python invocations to explicit `python3` (Jon Siwek, Corelight) 9 | 10 | 0.5 | 2020-01-28 12:06:18 -0800 11 | 12 | * Release 0.5. 13 | 14 | * Python 3, and broker API change updates for acld.py (Johanna Amann) 15 | 16 | * a bit of bro->zeek (Johanna Amann) 17 | 18 | * Update command-line example to python3. (Johanna Amann) 19 | 20 | This also fixes a non-matching topic names - which made the current 21 | state not work in an a tad surprising way. 22 | 23 | 0.4 | 2019-06-12 15:03:55 -0700 24 | 25 | * Release 0.4. 26 | 27 | 0.3-7 | 2019-06-12 12:02:19 -0700 28 | 29 | * Update README for new directory name (Daniel Thayer) 30 | 31 | 0.3-5 | 2019-05-28 14:05:51 -0700 32 | 33 | * GH-6: update openflow controller to more recent Broker API (Jon Siwek, Corelight) 34 | 35 | 0.3-4 | 2019-05-28 13:57:55 -0700 36 | 37 | * GH-7: Use tuples instead of lists to represent broker vectors (Jon Siwek, Corelight) 38 | 39 | 0.3-3 | 2019-05-20 18:59:12 -0700 40 | 41 | * Rename Bro to Zeek (Daniel Thayer) 42 | 43 | * Update git clone command in README (Jon Siwek, Corelight) 44 | 45 | 0.3 | 2018-05-21 18:47:21 +0000 46 | 47 | * Release 0.3. 48 | 49 | 0.2-11 | 2018-05-15 15:25:33 +0000 50 | 51 | * Remove duplicate lines. (Craig Leres) 52 | 53 | * You can now specify more than one "--acld_host" option to round robin 54 | across multiple ACLD servers to use different ACLD servers and 55 | ports. 56 | 57 | * Add a FreeBSD rc.d script. (Craig Leres) 58 | 59 | * Parse acld whitelist result as exists. (Johanna Amann) 60 | 61 | * Port python scripts to new Broker API, and update examples and 62 | documentation. (Corelight) 63 | 64 | 0.2 | 2016-09-07 10:51:41 +0200 65 | 66 | * Release 0.2. 67 | 68 | * Fix two small code issues reported by Jingyao Ma. (Johanna Amann) 69 | 70 | * Change readme to reflect that we now require the --enable-broker flag again. (Johanna Amann) 71 | 72 | 0.1 | 2016-08-10 10:11:52 -0700 73 | 74 | * Release 0.1. 75 | 76 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, The Regents of the University of California 2 | through the International Computer Science Institute. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | (1) Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | (2) Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | (3) Neither the name of the University of California, U.S. Dept. of Energy, 15 | International Computer Science Institute, nor the names of contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | 31 | Note that some files in the distribution may carry their own copyright 32 | notices. 33 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Zeek NetControl connector scripts 2 | ================================= 3 | 4 | This repository contains scripts that can be used to connect the Zeek NetControl 5 | framework to systems outside of Zeek and, e.g., send out switch commands via OpenFlow. 6 | 7 | Please note that the NetControl framework and scripts is still under active 8 | development; the API is not completely fixed yet and the scripts have not seen 9 | thorough testing. 10 | 11 | Installation Instructions 12 | ------------------------- 13 | 14 | To use the connector scripts, you need to install a current master version 15 | of Zeek with commands similar to this: 16 | 17 | git clone --recursive https://github.com/zeek/zeek 18 | cd zeek 19 | ./configure --prefix=[install prefix] 20 | make install 21 | 22 | To allow python to find the installed python Broker bindings, it might be 23 | necessary to adjust the PYTHONPATH variable similar to this: 24 | 25 | export PYTHONPATH=[install prefix]/lib/zeek/python:[this directory] 26 | 27 | after that, you should be able to launch the provided scripts. 28 | 29 | API 30 | --- 31 | 32 | The [netcontrol](netcontrol/) directory contains a python API for the Broker backend 33 | of the Zeek netcontrol framework. This API converts the Zeek data structures into python 34 | dictionaries and allows to send back success and error messages to Zeek. 35 | 36 | A simple [example script](test/simple-client.py) is provided in the [test](test/) 37 | directory. The API is also used by the command-line connector. 38 | 39 | Command-line connector 40 | ---------------------- 41 | 42 | The [command-line](command-line/) directory contains a script that can be used to 43 | interface the NetControl framework to command-line invocations. 44 | [commands.yaml](command-line/commands.yaml) shows an example that can be used to 45 | invoke iptables. An example script that simply blocks all connections is provided in 46 | [example.zeek](command-line/example.zeek). 47 | 48 | OpenFlow connector 49 | ------------------ 50 | 51 | The [openflow](openflow/) directory contains the source for a Ryu OpenFlow 52 | controller, that can be used to interface the Zeek NetControl framework with an 53 | OpenFlow capable switch. To use the controller, you need to first install the 54 | [Ryu SDN framework](https://ryu-sdn.org/). 55 | 56 | After installation, you can run the openflow controller by executing 57 | 58 | ryu-manager --verbose openflow/controller.py 59 | 60 | or similar. After that, OpenFlow switches should be able to connect to port 6633; 61 | Broker connections can be made to port 9999. An example script that shunts all 62 | connection traffic to a switch after an SSL, SSH or GridFTP session has been 63 | established is provided in [example.zeek](openflow/example.zeek). 64 | 65 | Acld connector 66 | -------------- 67 | 68 | The [acld](acld/) directory contains the source for an connector to [acld](ftp://ftp.ee.lbl.gov/acld.tar.gz) ([more information](http://ee.lbl.gov/leres/acl2.html)). 69 | An example script that simply blocks all connections is provided in 70 | [example.zeek](acld/example.zeek). 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6 2 | -------------------------------------------------------------------------------- /acld/acld.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Acld interface for the Network Control Framework of Zeek, using Broker. 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import errno 9 | import fcntl, os 10 | import logging 11 | import random 12 | import re 13 | import socket 14 | import string 15 | import sys 16 | import ipaddress 17 | import datetime 18 | import time 19 | 20 | import broker 21 | import broker.zeek 22 | from select import select 23 | from logging.handlers import TimedRotatingFileHandler 24 | 25 | MAX16INT = 2**16 - 1 26 | 27 | def parseArgs(): 28 | defaultuser = os.getlogin() 29 | defaulthost = socket.gethostname() 30 | defaultacldhost = '127.0.0.1' 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument('--listen', default="127.0.0.1", 34 | help="Address to listen on for connections (default: %(default)s)") 35 | parser.add_argument('--port', type=int, default=9999, 36 | help="Port to listen on for connections (default: %(default)s)") 37 | parser.add_argument('--acld_host', metavar='HOST', action='append', 38 | help='ACLD hosts to connect to (default: %s)' % defaultacldhost) 39 | parser.add_argument('--acld_port', metavar='PORT', type=int, default=11775, 40 | help="ACLD port to connect to (default: %(default)s)") 41 | parser.add_argument('--log-user', default=defaultuser, 42 | help='user name provided to acld (default: %(default)s)') 43 | parser.add_argument('--log-host', default=defaulthost, 44 | help='host name provided to acld (default: %(default)s)') 45 | parser.add_argument('--topic', default="zeek/event/pacf", 46 | help="Topic to subscribe to. (default: %(default)s)") 47 | parser.add_argument('--debug', const=logging.DEBUG, action='store_const', 48 | default=logging.INFO, 49 | help="Enable debug output") 50 | parser.add_argument('--logfile', 51 | help="Filename of logfile. If not given, logs to stdout") 52 | parser.add_argument('--rotate', action="store_true", 53 | help="If logging to file and --rotate is specified, log will rotate at midnight") 54 | 55 | args = parser.parse_args() 56 | if not args.acld_host: 57 | args.acld_host = [defaultacldhost] 58 | return args 59 | 60 | def hostportpair(host, port): 61 | """Host is an ip address or ip address and port, 62 | port is the default port. 63 | return a host-port pair""" 64 | tup = host.split(',', 1) 65 | if len(tup) == 2: 66 | host = tup[0] 67 | sport = tup[1] 68 | if not sport.isdigit(): 69 | self.logger.error('%s: port must be numeric' % host) 70 | sys.exit(-1) 71 | port = int(sport) 72 | if port <= 0 or port > MAX16INT: 73 | self.logger.error('%s: port must be > 0 and < %d ' % (host, MAX16INT)) 74 | sys.exit(-1) 75 | return host, port 76 | 77 | class Listen(object): 78 | TIMEOUT_INITIAL = 0.25 79 | TIMEOUT_MAX = 8.0 80 | 81 | def __init__(self, queue, host, port, acld_hosts, acld_port, log_user, log_host): 82 | self.logger = logging.getLogger("brokerlisten") 83 | 84 | self.queuename = queue 85 | self.epl = broker.Endpoint() 86 | self.epl.listen(host, port) 87 | self.status_subscriber = self.epl.make_status_subscriber(True) 88 | self.subscriber = self.epl.make_subscriber(self.queuename) 89 | 90 | # Create a random list of host port pairs 91 | self.acld_hosts = [] 92 | for host in acld_hosts: 93 | self.acld_hosts.append(hostportpair(host, acld_port)) 94 | random.shuffle(self.acld_hosts) 95 | 96 | self.ident = '{%s@%s}' % (log_user, log_host) 97 | self.remote_ident = '?' 98 | 99 | self.sock = None 100 | 101 | self.waiting = {} 102 | self.buffer = '' 103 | 104 | self.acldstring = False 105 | self.acldcmd = {} 106 | 107 | # try to connect to acld 108 | self.connect() 109 | 110 | def connect(self): 111 | """Round robin across multiple aclds with exponential backoff""" 112 | if self.sock: 113 | self.sock.close() 114 | self.sock = None; 115 | # No delay on first retry with multiple aclds 116 | if len(self.acld_hosts) > 1: 117 | timeout = 0.0 118 | else: 119 | timeout = self.TIMEOUT_INITIAL 120 | while True: 121 | self.sock = socket.socket() 122 | hostpair = self.get_hostpair() 123 | self.remote_ident = '[%s].%d' % (hostpair[0], hostpair[1]) 124 | self.logger.debug('%s Connecting' % self.remote_ident) 125 | try: 126 | self.sock.connect(hostpair) 127 | except socket.error as e: 128 | self.logger.error('%s %s' % (self.remote_ident, e.strerror)) 129 | time.sleep(timeout) 130 | if not timeout: 131 | timeout = self.TIMEOUT_INITIAL 132 | else: 133 | timeout *= 2 134 | if timeout > self.TIMEOUT_MAX: 135 | timeout = self.TIMEOUT_MAX 136 | continue 137 | 138 | fcntl.fcntl(self.sock, fcntl.F_SETFL, os.O_NONBLOCK) 139 | self.logger.info('%s Connected' % self.remote_ident) 140 | break 141 | 142 | def get_hostpair(self): 143 | """Round robin multiple ACLD hosts""" 144 | hostport = self.acld_hosts.pop(0) 145 | self.acld_hosts.append(hostport) 146 | return hostport 147 | 148 | def listen_loop(self): 149 | self.logger.debug("Broker loop...") 150 | 151 | while 1==1: 152 | self.logger.debug("Waiting for broker message") 153 | readable, writable, exceptional = select( 154 | [self.status_subscriber.fd(), 155 | self.subscriber.fd(), self.sock], 156 | [], []) 157 | 158 | if ( self.status_subscriber.fd() in readable ): 159 | self.logger.debug("Got broker status message") 160 | msg = self.status_subscriber.get() 161 | self._handle_broker_message(msg) 162 | elif ( self.subscriber.fd() in readable ): 163 | self.logger.debug("Got broker message") 164 | msg = self.subscriber.get() 165 | self._handle_broker_message(msg) 166 | elif ( self.sock in readable ): 167 | line = self.read_acld() 168 | while line != None: 169 | self.logger.info("Received from ACLD: %s", line) 170 | self.parse_acld(line) 171 | line = self.read_acld() 172 | continue 173 | 174 | 175 | def parse_acld(self, line): 176 | line = line.rstrip("\r") 177 | if self.acldstring == False: 178 | items = line.split(" ") 179 | if len(items) == 3: 180 | ts, cookie, command = items 181 | more = None 182 | elif len(items) == 4: 183 | ts, cookie, command, more = items 184 | else: 185 | self.logger.error("Could not parse acld line: %s", line) 186 | return 187 | 188 | self.acldcmd = {'ts': ts, 'cookie': cookie, 'command': command} 189 | if more != None: 190 | self.acldstring = True 191 | self.acldcmd['comment'] = "" 192 | if more != "-": 193 | self.logger.error("Parse error while parsing acld line: %s?", more) 194 | else: 195 | self.execute_acld() 196 | else: 197 | if line == ".": 198 | self.acldstring = False 199 | self.execute_acld() 200 | else: 201 | self.acldcmd['comment']+=line 202 | 203 | def execute_acld(self): 204 | cmd = self.acldcmd['command'] 205 | cookie = int(self.acldcmd['cookie']) 206 | comment = self.acldcmd.get('comment', "") 207 | 208 | if cmd == "acld": 209 | # we get this when connecting 210 | self.logger.info('%s acld connection succesful' % self.remote_ident) 211 | return 212 | 213 | if cookie in self.waiting: 214 | msg = self.waiting[cookie] 215 | del self.waiting[cookie] 216 | 217 | if "-failed" in cmd: 218 | if re.search(".* is on the whitelist .*", comment): 219 | self.rule_event("exists", msg['id'], msg['arule'], msg['rule'], comment) 220 | else: 221 | self.rule_event("error", msg['id'], msg['arule'], msg['rule'], comment) 222 | elif re.search("Note: .* is already ", comment): 223 | self.rule_event("exists", msg['id'], msg['arule'], msg['rule'], comment) 224 | else: 225 | type = "added" 226 | if msg['add'] == False: 227 | type = "removed" 228 | self.rule_event(type, msg['id'], msg['arule'], msg['rule'], comment) 229 | 230 | else: 231 | self.logger.warning("Got response to cookie %d we did not send. Ignoring", cookie) 232 | return 233 | 234 | def read_acld(self): 235 | try: 236 | data = self.sock.recv(4096) 237 | if len(data) == 0: 238 | self.logger.warning('%s Disconnected' % self.remote_ident) 239 | self.connect() 240 | self.buffer += data.decode("utf-8") 241 | except socket.error as e: 242 | err = e.args[0] 243 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 244 | # socket not ready yet, just continue and see if something 245 | # is still in the buffer 246 | pass 247 | else: 248 | self.logger.error(e) 249 | sys.exit(-1) 250 | return 251 | 252 | if self.buffer.find("\r\n") != -1: 253 | line, self.buffer = self.buffer.split("\r\n", 1) 254 | return line 255 | else: 256 | return None 257 | 258 | def _handle_broker_message(self, m): 259 | if isinstance(m, broker.Status): 260 | if m.code() == broker.SC.PeerAdded: 261 | self.logger.info("Incoming connection established.") 262 | return 263 | 264 | return 265 | 266 | if type(m).__name__ != "tuple": 267 | self.logger.error("Unexpected type %s, expected tuple.", type(m).__name__) 268 | return 269 | 270 | if len(m) < 1: 271 | self.logger.error("Tuple without content?") 272 | return 273 | 274 | (topic, event) = m 275 | ev = broker.zeek.Event(event) 276 | event_name = ev.name() 277 | 278 | if event_name == "NetControl::acld_add_rule": 279 | self.add_remove_rule(event_name, ev.args(), True) 280 | elif event_name == "NetControl::acld_remove_rule": 281 | self.add_remove_rule(event_name, ev.args(), False) 282 | elif event_name == "NetControl::acld_rule_added": 283 | pass 284 | elif event_name == "NetControl::acld_rule_removed": 285 | pass 286 | elif event_name == "NetControl::acld_rule_error": 287 | pass 288 | elif event_name == "NetControl::acld_rule_exists": 289 | pass 290 | else: 291 | self.logger.error("Unknown event %s", event_name) 292 | return 293 | 294 | def add_remove_rule(self, name, m, add): 295 | print(m) 296 | if ( len(m) != 3 ) or ( not isinstance(m[0], broker.Count) ) or (not isinstance(m[1], tuple) ) or ( not isinstance(m[2], tuple) ): 297 | self.logger.error("wrong number of elements or type in tuple for acld_add|remove_rule") 298 | return 299 | 300 | id = m[0].value 301 | arule = self.record_to_record("acldrule", m[2]) 302 | 303 | self.logger.info("Got event %s. id=%d, arule: %s", name, id, arule) 304 | 305 | cmd = arule['command'] + " " + str(arule['cookie']) + " " + arule['arg'] + " -" 306 | sendlist = [cmd, self.ident] 307 | if 'comment' in arule and arule['comment'] != None and len(arule['comment']) > 0: 308 | sendlist.append(arule['comment']) 309 | sendlist.append(".") 310 | 311 | self.waiting[arule['cookie']] = {'add': add, 'cmd': cmd, 'id': m[0], 'rule': m[1], 'arule': m[2]} 312 | self.logger.info("Sending to ACLD: %s", ", ".join(sendlist)) 313 | self.sock.sendall(("\r\n".join(sendlist)+"\r\n").encode()) 314 | 315 | def rule_event(self, event, id, arule, rule, msg): 316 | arule = self.record_to_record("acldrule", arule) 317 | self.logger.info("Sending to Zeek: NetControl::acld_rule_%s id=%d, arule=%s, msg=%s", event, id.value, arule, msg) 318 | 319 | ev = broker.zeek.Event("NetControl::acld_rule_"+event, id, rule, msg) 320 | self.epl.publish(self.queuename, ev) 321 | 322 | def record_to_record(self, name, m): 323 | if not isinstance(m, tuple): 324 | self.logger.error("Got non record element") 325 | 326 | rec = m 327 | 328 | elements = None 329 | if name == "acldrule": 330 | elements = ['command', 'cookie', 'arg', 'comment'] 331 | else: 332 | self.logger.error("Unknown record type %s", name) 333 | return 334 | 335 | dict = {} 336 | for i in range(0, len(elements)): 337 | if rec[i] is None: 338 | dict[elements[i]] = None 339 | continue 340 | elif isinstance(rec[i], tuple): 341 | dict[elements[i]] = self.record_to_record(name+"->"+elements[i], rec[i]) 342 | continue 343 | 344 | dict[elements[i]] = self.convert_element(rec[i]) 345 | 346 | return dict 347 | 348 | def convert_element(self, el): 349 | if isinstance(el, broker.Count): 350 | return el.value 351 | 352 | if isinstance(el, ipaddress.IPv4Address): 353 | return str(el); 354 | 355 | if isinstance(el, ipaddress.IPv6Address): 356 | return str(el); 357 | 358 | if isinstance(el, ipaddress.IPv4Network): 359 | return str(el); 360 | 361 | if isinstance(el, ipaddress.IPv6Network): 362 | return str(el); 363 | 364 | if isinstance(el, broker.Port): 365 | p = str(el) 366 | ex = re.compile('([0-9]+)(.*)') 367 | res = ex.match(p) 368 | return (res.group(1), res.group(2)) 369 | 370 | if isinstance(el, broker.Enum): 371 | tmp = el.name 372 | return re.sub(r'.*::', r'', tmp) 373 | 374 | if isinstance(el, list): 375 | return [convertElement(ell) for ell in el]; 376 | 377 | if isinstance(el, datetime.datetime): 378 | return el 379 | 380 | if isinstance(el, datetime.timedelta): 381 | return el 382 | 383 | if isinstance(el, int): 384 | return el 385 | 386 | if isinstance(el, str): 387 | return el 388 | 389 | logger.error("Unsupported type %s", type(el) ) 390 | return el; 391 | 392 | args = parseArgs() 393 | logger = logging.getLogger('') 394 | logger.setLevel(args.debug) 395 | 396 | handler = None 397 | 398 | if args.logfile: 399 | if args.rotate: 400 | handler = TimedRotatingFileHandler(args.logfile, 'midnight') 401 | else: 402 | handler = logging.FileHandler(args.logfile); 403 | else: 404 | handler = logging.StreamHandler(sys.stdout) 405 | 406 | formatter = logging.Formatter('%(created).6f:%(name)s:%(levelname)s:%(message)s') 407 | handler.setFormatter(formatter) 408 | 409 | logger.addHandler(handler) 410 | 411 | logging.info("Starting acld.py...") 412 | brocon = Listen(args.topic, args.listen, args.port, args.acld_host, 413 | args.acld_port, args.log_user, args.log_host) 414 | try: 415 | brocon.listen_loop() 416 | except KeyboardInterrupt: 417 | pass 418 | -------------------------------------------------------------------------------- /acld/broker-acld-rc.d.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # @(#) $Id: broker-acld,v 1.8 2018/04/25 17:26:04 leres Exp $ (LBL) 3 | # 4 | 5 | # PROVIDE: broker-acld 6 | # REQUIRE: LOGIN 7 | # KEYWORD: shutdown 8 | # 9 | # Variables that can be set in /etc/rc.conf: 10 | # 11 | # broker_acld_enable 12 | # broker_acld_asuser user to run as 13 | # broker_acld_netcontrol path to net-control directory 14 | # broker_acld_pidfile 15 | # broker_acld_hosts one or more acld hosts 16 | # broker_acld_port default acld port 17 | # broker_acld_logfile path to logfile 18 | # 19 | # broker_acld_hosts hosts can override the default port, e.g. 127.0.0.1,1234 20 | # 21 | 22 | . /etc/rc.subr 23 | 24 | name=broker_acld 25 | rcvar=broker_acld_enable 26 | 27 | load_rc_config "$name" 28 | 29 | asuser=${broker_acld_asuser:-bro} 30 | netcontrol=${broker_acld_netcontrol:-/home/bro/bro-netcontrol} 31 | pidfile="${broker_acld_pidfile:-/var/run/broker-acld.pid}" 32 | 33 | broker_acld_enable=${broker_acld_enable:-"NO"} 34 | broker_acld_hosts=${broker_acld_hosts:-127.0.0.1} 35 | broker_acld_port=${broker_acld_port:-1965} 36 | broker_acld_logfile=${broker_acld_logfile:-${netcontrol}/acld/broker-logs} 37 | broker_acld_env="PYTHONPATH=${netcontrol}/lib/python" 38 | 39 | command=/usr/sbin/daemon 40 | command_interpreter=python3 41 | procname=${broker_acld_program:-${netcontrol}/acld/broker-acld.py} 42 | unset broker_acld_program 43 | 44 | command_args="-u ${asuser} -p ${pidfile} ${procname}" 45 | for host in ${broker_acld_hosts}; do 46 | command_args="${command_args} --acld_host ${host}" 47 | done 48 | command_args="${command_args} --acld_port ${broker_acld_port}" 49 | command_args="${command_args} --logfile ${broker_acld_logfile}" 50 | command_args="${command_args} --rotate" 51 | command_args="${command_args} ${broker_acld_flags}" 52 | 53 | run_rc_command "$1" 54 | -------------------------------------------------------------------------------- /acld/example.zeek: -------------------------------------------------------------------------------- 1 | @load base/protocols/conn 2 | @load base/frameworks/netcontrol 3 | 4 | const broker_port: port = 9999/tcp &redef; 5 | 6 | event NetControl::init() 7 | { 8 | local netcontrol_acld = NetControl::create_acld([$acld_host=127.0.0.1, $acld_port=broker_port, $acld_topic="zeek/event/pacf"]); 9 | NetControl::activate(netcontrol_acld, 0); 10 | } 11 | 12 | event NetControl::init_done() 13 | { 14 | print "NeControl is starting operations"; 15 | } 16 | 17 | event Broker::peer_added(endpoint: Broker::EndpointInfo, msg: string) 18 | { 19 | print "Broker peer added", endpoint$network; 20 | } 21 | 22 | event NetControl::rule_added(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 23 | { 24 | print "Rule added successfully", r$id, msg; 25 | } 26 | 27 | event NetControl::rule_error(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 28 | { 29 | print "Rule error", r$id, msg; 30 | } 31 | 32 | event NetControl::rule_timeout(r: NetControl::Rule, i: NetControl::FlowInfo, p: NetControl::PluginState) 33 | { 34 | print "Rule timeout", r$id, i; 35 | } 36 | 37 | event NetControl::rule_removed(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 38 | { 39 | print "Rule removed", r$id, msg; 40 | } 41 | 42 | event connection_established(c: connection) 43 | { 44 | local id = c$id; 45 | local flow = NetControl::Flow( 46 | $src_h=addr_to_subnet(id$orig_h), 47 | $dst_h=addr_to_subnet(id$resp_h) 48 | ); 49 | local e: NetControl::Entity = [$ty=NetControl::FLOW, $flow=flow]; 50 | local r: NetControl::Rule = [$ty=NetControl::DROP, $target=NetControl::FORWARD, $entity=e, $expire=20sec]; 51 | 52 | NetControl::add_rule(r); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /command-line/command-line.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Command-line interface for the Network Control Framework of Zeek, using Broker. 4 | 5 | import logging 6 | import netcontrol 7 | import sys 8 | import _thread 9 | from yaml import load 10 | try: 11 | from yaml import CLoader as Loader 12 | except ImportError: 13 | from yaml import Loader 14 | import string 15 | import re 16 | import argparse 17 | import sys 18 | 19 | from subprocess import check_output 20 | from subprocess import CalledProcessError 21 | #from future.utils import viewitems 22 | 23 | from enum import Enum, unique 24 | 25 | def parseArgs(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('--listen', default="127.0.0.1", help="Address to listen on for connections. Default: 127.0.0.1") 28 | parser.add_argument('--port', default=9977, help="Port to listen on for connections. Default: 9977") 29 | parser.add_argument('--topic', default="zeek/event/netcontrol-example", help="Topic to subscribe to. Default: zeek/event/netcontrol-example") 30 | parser.add_argument('--file', default="commands.yaml", help="File to read commands from. Default: commands.yaml") 31 | parser.add_argument('--debug', const=logging.DEBUG, default=logging.INFO, action='store_const', help="Enable debug output") 32 | 33 | args = parser.parse_args() 34 | return args 35 | 36 | class Listen: 37 | def __init__(self, queue, host, port, commands, **kwargs): 38 | self.logger = logging.getLogger(__name__) 39 | self.endpoint = netcontrol.Endpoint(queue, host, port) 40 | self.queuename = queue 41 | self.commands = commands 42 | self.use_threads = kwargs.get('use_threads', True) 43 | 44 | def listen_loop(self): 45 | self.logger.debug("Listen loop...") 46 | 47 | while 1==1: 48 | response = self.endpoint.getNextCommand() 49 | 50 | if response.type == netcontrol.ResponseType.AddRule: 51 | if self.use_threads: 52 | _thread.start_new_thread(self._add_remove_rule, (response, )) 53 | else: 54 | self._add_remove_rule(response) 55 | if response.type == netcontrol.ResponseType.RemoveRule: 56 | if self.use_threads: 57 | _thread.start_new_thread(self._add_remove_rule, (response, )) 58 | else: 59 | self._add_remove_rule(response) 60 | 61 | def _add_remove_rule(self, response): 62 | 63 | cmd = self.rule_to_cmd_dict(response.rule) 64 | 65 | if response.type == netcontrol.ResponseType.AddRule: 66 | type = 'add_rule' 67 | elif response.type == netcontrol.ResponseType.RemoveRule: 68 | type = 'remove_rule' 69 | else: 70 | self.logger.error("Internal error - incompabible rule type") 71 | type = 'unknown' 72 | 73 | if not ( type in self.commands): 74 | self.logger.error("No %s in commands", type) 75 | return 76 | 77 | commands = self.commands[type] 78 | 79 | output = "" 80 | 81 | self.logger.info("Received %s from Zeek: %s", type, cmd) 82 | 83 | for i in commands: 84 | currcmd = self.replace_command(i, cmd) 85 | output += "Command: "+currcmd+"\n" 86 | 87 | try: 88 | self.logger.info("Executing "+currcmd) 89 | cmdout = check_output(currcmd, shell=True) 90 | output += "Output: "+str(cmdout)+"\n" 91 | self.logger.debug("Command executed succefsully") 92 | except CalledProcessError as err: 93 | output = "Command "+currcmd+" failed with return code "+str(err.returncode)+" and output: "+str(err.output) 94 | self.logger.error(output) 95 | self.endpoint.sendRuleError(response, output) 96 | return 97 | except OSError as err: 98 | output = "Command "+currcmd+" failed with error code "+str(err.errno)+" ("+err.strerror+")" 99 | sel.logger.error(output) 100 | self.endpoint.sendRuleError(response, output) 101 | return 102 | 103 | if response.type == netcontrol.ResponseType.AddRule: 104 | self.logger.info("Sending rule_added to Zeek") 105 | self.endpoint.sendRuleAdded(response, output) 106 | else: 107 | self.logger.info("Sending rule_removed to Zeek") 108 | self.endpoint.sendRuleRemoved(response, output) 109 | 110 | def replace_single_command(self, argstr, cmds): 111 | reg = re.compile('\[(?P.)(?P.*?)(?:\:(?P.*?))?\]') 112 | #print argstr 113 | m = reg.search(argstr) 114 | 115 | if m == None: 116 | self.logger.error('%s could not be converted to rule', argstr) 117 | return '' 118 | 119 | type = m.group('type') 120 | target = m.group('target') 121 | arg = m.group('argument') 122 | 123 | if type == '?': 124 | if not ( target in cmds ): 125 | return '' 126 | elif arg == None: 127 | return cmds[target] 128 | 129 | # we have an argument *sigh* 130 | return re.sub(r'\.', cmds[target], arg) 131 | elif type == '!': 132 | if arg == None: 133 | self.logger.error("[!] needs argument for %s", argstr) 134 | return '' 135 | 136 | if not ( target in cmds ): 137 | return arg 138 | else: 139 | return '' 140 | else: 141 | self.logger.error("unknown command type %s in %s", type, argstr) 142 | return '' 143 | 144 | def replace_command(self, command, args): 145 | reg = re.compile('\[(?:\?|\!).*?\]') 146 | 147 | return reg.sub(lambda x: self.replace_single_command(x.group(), args), command) 148 | 149 | def rule_to_cmd_dict(self, rule): 150 | cmd = {} 151 | 152 | mapping = { 153 | 'type': 'ty', 154 | 'target': 'target', 155 | 'expire': 'expire', 156 | 'priority': 'priority', 157 | 'id': 'id', 158 | 'cid': 'cid', 159 | 'entity.ip': 'address', 160 | 'entity.mac': 'mac', 161 | 'entity.conn.orig_h': 'conn.orig_h', 162 | 'entity.conn.orig_p': 'conn.orig_p', 163 | 'entity.conn.resp_h': 'conn.resp_h', 164 | 'entity.conn.resp_p': 'conn.resp_p', 165 | 'entity.flow.src_h': 'flow.src_h', 166 | 'entity.flow.src_p': 'flow.src_p', 167 | 'entity.flow.dst_h': 'flow.dst_h', 168 | 'entity.flow.dst_p': 'flow.dst_p', 169 | 'entity.flow.src_m': 'flow.src_m', 170 | 'entity.flow.dst_m': 'flow.dst_m', 171 | 'entity.mod.src_h': 'mod.src_h', 172 | 'entity.mod.src_p': 'mod.src_p', 173 | 'entity.mod.dst_h': 'mod.dst_h', 174 | 'entity.mod.dst_p': 'mod.dst_p', 175 | 'entity.mod.src_m': 'mod.src_m', 176 | 'entity.mod.dst_m': 'mod.dst_m', 177 | 'entity.mod.redirect_port': 'mod.port', 178 | 'entity.i': 'mod.port', 179 | } 180 | 181 | for (k, v) in list(mapping.items()): 182 | path = k.split('.') 183 | e = rule 184 | for i in path: 185 | if e == None: 186 | break 187 | elif i in e: 188 | e = e[i] 189 | else: 190 | e = None 191 | break 192 | 193 | if e == None: 194 | continue 195 | 196 | if isinstance(e, tuple): 197 | cmd[v] = e[0] 198 | cmd[v+".proto"] = e[1] 199 | else: 200 | cmd[v] = e 201 | if isinstance(e, str): 202 | spl = e.split("/") 203 | if len(spl) > 1: 204 | cmd[v+".ip"] = spl[0] 205 | cmd[v+".net"] = spl[1] 206 | 207 | proto = mapping.get('entity.conn.orig_p.proto', mapping.get('entity.conn.dest_p.proto', mapping.get('entity.flow.src_p.proto', mapping.get('entity.flow.dst_p.proto', None)))) 208 | if proto != None: 209 | entity['proto'] = proto 210 | 211 | return cmd 212 | 213 | args = parseArgs() 214 | 215 | stream = open(args.file, 'r') 216 | config = load(stream, Loader=Loader) 217 | 218 | logging.basicConfig(level=args.debug) 219 | 220 | logging.info("Starting command-line client...") 221 | zeekcon = Listen(args.topic, args.listen, int(args.port), config) 222 | zeekcon.listen_loop() 223 | 224 | -------------------------------------------------------------------------------- /command-line/commands.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | add_rule: 3 | - 'echo iptables -A INPUT [?address:-s . ][?conn.orig_h:-s . ][?conn.orig_p: --sport . ][?flow.src_h: -s . ][?flow.src_p: --sport .] [?conn.resp_h:-d . ][?conn.resp_p: --dport . ][?flow.dst_h: -d . ][?flow.dst_p: --dport . ] -j DROP' 4 | remove_rule: 5 | - 'echo iptables -D INPUT [?address:-s . ][?conn.orig_h:-s . ][?conn.orig_p: --sport . ][?flow.src_h: -s . ][?flow.src_p: --sport .] [?conn.resp_h:-d . ][?conn.resp_p: --dport . ][?flow.dst_h: -d . ][?flow.dst_p: --dport . ] -j DROP' 6 | -------------------------------------------------------------------------------- /command-line/example.zeek: -------------------------------------------------------------------------------- 1 | @load base/protocols/conn 2 | @load base/frameworks/netcontrol 3 | 4 | const broker_port: port = 9977/tcp &redef; 5 | 6 | event NetControl::init() 7 | { 8 | local netcontrol_broker = NetControl::create_broker(NetControl::BrokerConfig($host=127.0.0.1, $bport=broker_port, $topic="zeek/event/netcontrol-example"), F); 9 | NetControl::activate(netcontrol_broker, 0); 10 | } 11 | 12 | event NetControl::init_done() 13 | { 14 | print "NeControl is starting operations"; 15 | } 16 | 17 | event Broker::peer_added(endpoint: Broker::EndpointInfo, msg: string) 18 | { 19 | print "Broker peer added", endpoint$network; 20 | } 21 | 22 | event NetControl::rule_added(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 23 | { 24 | print "Rule added successfully", r$id, msg; 25 | } 26 | 27 | event NetControl::rule_error(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 28 | { 29 | print "Rule error", r$id, msg; 30 | } 31 | 32 | event NetControl::rule_timeout(r: NetControl::Rule, i: NetControl::FlowInfo, p: NetControl::PluginState) 33 | { 34 | print "Rule timeout", r$id, i; 35 | } 36 | 37 | event connection_established(c: connection) 38 | { 39 | local id = c$id; 40 | local flow = NetControl::Flow( 41 | $src_h=addr_to_subnet(id$orig_h), 42 | $dst_h=addr_to_subnet(id$resp_h) 43 | ); 44 | local e: NetControl::Entity = [$ty=NetControl::FLOW, $flow=flow]; 45 | local r: NetControl::Rule = [$ty=NetControl::DROP, $target=NetControl::FORWARD, $entity=e, $expire=20sec]; 46 | 47 | NetControl::add_rule(r); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /netcontrol/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * 2 | -------------------------------------------------------------------------------- /netcontrol/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import ipaddress 4 | import datetime 5 | import broker 6 | import broker.zeek 7 | from select import select 8 | from enum import Enum, unique 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | def convertRecord(name, m): 13 | if not isinstance(m, tuple): 14 | logger.error("Got non record element") 15 | 16 | rec = m 17 | 18 | elements = None 19 | if name == "rule": 20 | elements = ['ty', 'target', 'entity', 'expire', 'priority', 'location', 'out_port', 'mod', 'id', 'cid'] 21 | elif name == "rule->entity": 22 | elements = ['ty', 'conn', 'flow', 'ip', 'mac'] 23 | elif name == "rule->entity->conn": 24 | elements = ['orig_h', 'orig_p', 'resp_h', 'resp_p'] 25 | elif name == "rule->entity->flow": 26 | elements = ['src_h', 'src_p', 'dst_h', 'dst_p', 'src_m', 'dst_m'] 27 | elif name == "rule->mod": 28 | elements = ['src_h', 'src_p', 'dst_h', 'dst_p', 'src_m', 'dst_m', 'redirect_port'] 29 | else: 30 | logger.error("Unknown record type %s", name) 31 | return 32 | 33 | dict = {} 34 | for i in range(0, len(elements)): 35 | if rec[i] is None: 36 | dict[elements[i]] = None 37 | continue 38 | elif isinstance(rec[i], tuple): 39 | dict[elements[i]] = convertRecord(name+"->"+elements[i], rec[i]) 40 | continue 41 | 42 | dict[elements[i]] = convertElement(rec[i]) 43 | 44 | return dict 45 | 46 | def convertElement(el): 47 | if isinstance(el, broker.Count): 48 | return el.value 49 | 50 | if isinstance(el, ipaddress.IPv4Address): 51 | return str(el); 52 | 53 | if isinstance(el, ipaddress.IPv6Address): 54 | return str(el); 55 | 56 | if isinstance(el, ipaddress.IPv4Network): 57 | return str(el); 58 | 59 | if isinstance(el, ipaddress.IPv6Network): 60 | return str(el); 61 | 62 | if isinstance(el, broker.Port): 63 | p = str(el) 64 | ex = re.compile('([0-9]+)(.*)') 65 | res = ex.match(p) 66 | return (res.group(1), res.group(2)) 67 | 68 | if isinstance(el, broker.Enum): 69 | tmp = el.name 70 | return re.sub(r'.*::', r'', tmp) 71 | 72 | if isinstance(el, tuple): 73 | return tuple(convertElement(ell) for ell in el); 74 | 75 | if isinstance(el, datetime.datetime): 76 | return el 77 | 78 | if isinstance(el, datetime.timedelta): 79 | return el 80 | 81 | if isinstance(el, int): 82 | return el 83 | 84 | if isinstance(el, str): 85 | return el 86 | 87 | logger.error("Unsupported type %s", type(el) ) 88 | return el; 89 | 90 | @unique 91 | class ResponseType(Enum): 92 | ConnectionEstablished = 1 93 | Error = 2 94 | AddRule = 3 95 | RemoveRule = 4 96 | SelfEvent = 5 97 | 98 | class NetControlResponse: 99 | def __init__(self): 100 | self.type = (ResponseType.Error) 101 | self.errormsg = "" 102 | self.rule = "" 103 | 104 | def __init__(self, rty, **kwargs): 105 | self.type = rty 106 | self.errormsg = kwargs.get('errormsg', '') 107 | self.pluginid = kwargs.get('pluginid', None) 108 | self.rule = kwargs.get('rule', None) 109 | self.rawrule = kwargs.get('rawrule', None) 110 | 111 | class Endpoint: 112 | def __init__(self, queue, host, port): 113 | self.queuename = queue 114 | self.epl = broker.Endpoint() 115 | self.epl.listen(host, port) 116 | self.status_subscriber = self.epl.make_status_subscriber(True) 117 | self.subscriber = self.epl.make_subscriber(self.queuename) 118 | 119 | logger.debug("Set up listener for "+host+":"+str(port)+" ("+queue+")") 120 | def getNextCommand(self): 121 | while True: 122 | logger.debug("Waiting for broker message...") 123 | readable, writable, exceptional = select( 124 | [self.status_subscriber.fd(), self.subscriber.fd()], 125 | [], []) 126 | 127 | if ( self.status_subscriber.fd() in readable ): 128 | logger.debug("Handling broker status message...") 129 | msg = self.status_subscriber.get() 130 | 131 | if isinstance(msg, broker.Status): 132 | if msg.code() == broker.SC.PeerAdded: 133 | logger.info("Incoming connection established") 134 | return NetControlResponse(ResponseType.ConnectionEstablished) 135 | 136 | continue 137 | 138 | elif ( self.subscriber.fd() in readable ): 139 | logger.debug("Handling broker message...") 140 | msg = self.subscriber.get() 141 | return self.handleBrokerMessage(msg) 142 | 143 | def handleBrokerMessage(self, m): 144 | if type(m).__name__ != "tuple": 145 | logger.error("Unexpected type %s, expected tuple", type(m).__name__) 146 | return NetControlResponse(ResponseType.Error) 147 | 148 | if len(m) < 1: 149 | logger.error("Tuple without content?") 150 | return NetControlResponse(ResponseType.Error) 151 | 152 | (topic, event) = m 153 | ev = broker.zeek.Event(event) 154 | 155 | event_name = ev.name() 156 | logger.debug("Got event "+event_name) 157 | 158 | if event_name == "NetControl::broker_add_rule": 159 | return self._add_remove_rule(ev.args(), ResponseType.AddRule) 160 | elif event_name == "NetControl::broker_remove_rule": 161 | return self._add_remove_rule(ev.args(), ResponseType.RemoveRule) 162 | elif event_name == "NetControl::broker_rule_added": 163 | return NetControlResponse(ResponseType.SelfEvent) 164 | elif event_name == "NetControl::broker_rule_removed": 165 | return NetControlResponse(ResponseType.SelfEvent) 166 | elif event_name == "NetControl::broker_rule_error": 167 | return NetControlResponse(ResponseType.SelfEvent) 168 | elif event_name == "NetControl::broker_rule_timeout": 169 | return NetControlResponse(ResponseType.SelfEvent) 170 | else: 171 | logger.warning("Unknown event %s", event_name) 172 | return NetControlResponse(ResponseType.Error, errormsg="Unknown event"+event_name) 173 | 174 | def _add_remove_rule(self, m, rtype): 175 | if ( (rtype == ResponseType.AddRule) and ( len(m) != 2 ) ) or ( (rtype == ResponseType.RemoveRule) and ( len(m) != 3 ) ): 176 | logger.error("wrong number of elements or type in tuple for add/remove_rule event") 177 | return NetControlResponse(ResponseType.Error, errormsg="wrong number of elements or type in tuple for add/remove_rule event") 178 | 179 | if ( not isinstance(m[0], broker.Count) or 180 | not isinstance(m[1], tuple) ): 181 | logger.error("wrong types of elements or type in tuple for add/remove_rule event") 182 | return NetControlResponse(ResponseType.Error, errormsg="wrong types of elements or type in tuple for add/remove_rule event") 183 | 184 | id = m[0].value 185 | rule = convertRecord("rule", m[1]) 186 | 187 | return NetControlResponse(rtype, pluginid=id, rule=rule, rawrule=m[1]) 188 | 189 | def sendRuleAdded(self, response, msg): 190 | self._rule_event("added", response, msg) 191 | 192 | def sendRuleRemoved(self, response, msg): 193 | self._rule_event("removed", response, msg) 194 | 195 | def sendRuleError(self, response, msg): 196 | self._rule_event("error", response, msg) 197 | 198 | def _rule_event(self, event, response, msg): 199 | args = [broker.Count(response.pluginid), response.rawrule, msg] 200 | ev = broker.zeek.Event("NetControl::broker_rule_"+event, *args) 201 | self.epl.publish(self.queuename, ev) 202 | -------------------------------------------------------------------------------- /openflow/controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Ryu OpenFlow controller that connects to the Zeek OpenFlow 4 | # framework using Broker. 5 | # 6 | # Start with ./ryu/bin/ryu-manager controller.py 7 | 8 | import datetime 9 | import ipaddress 10 | import logging 11 | import time 12 | import re 13 | import ryu.app.ofctl.api 14 | from netaddr import IPNetwork 15 | 16 | from ryu.base import app_manager 17 | from ryu.controller import dpset 18 | from ryu.controller import controller 19 | from ryu.controller import ofp_event 20 | from ryu.controller.handler import MAIN_DISPATCHER 21 | from ryu.controller.handler import CONFIG_DISPATCHER 22 | from ryu.controller.handler import set_ev_cls 23 | from ryu.ofproto import ofproto_v1_0 24 | from ryu.ofproto import ofproto_v1_2 25 | from ryu.ofproto import ofproto_v1_3 26 | from ryu.lib import ofctl_v1_0 27 | from ryu.lib import ofctl_v1_2 28 | from ryu.lib import ofctl_v1_3 29 | from ryu.lib import hub 30 | 31 | import broker 32 | from select import select 33 | 34 | supported_ofctl = { 35 | ofproto_v1_0.OFP_VERSION: ofctl_v1_0, 36 | ofproto_v1_2.OFP_VERSION: ofctl_v1_2, 37 | ofproto_v1_3.OFP_VERSION: ofctl_v1_3, 38 | } 39 | 40 | queuename = "zeek/openflow" 41 | 42 | # for monkey-patching. 43 | # Barf. 44 | def zeek_send_msg(self, msg): 45 | assert isinstance(msg, self.ofproto_parser.MsgBase) 46 | 47 | if not hasattr(self, 'zeeksend'): 48 | self.zeeksend = 0 49 | self.zeekmessage = None 50 | 51 | # if we set before that we just want the message returned 52 | # without sending it on - return it to us so we can use 53 | # it further... 54 | if ( self.zeeksend == 1 ): 55 | self.zeeksend = 0 56 | self.zeekmessage = msg 57 | return msg 58 | 59 | self.send_msg_orig(msg) 60 | 61 | # sorry about all that. This is just to get the API somewhere where we actually can work 62 | # with it. 63 | # :/ 64 | ryu.controller.controller.Datapath.send_msg_orig = ryu.controller.controller.Datapath.send_msg 65 | ryu.controller.controller.Datapath.send_msg = zeek_send_msg 66 | 67 | class ZeekController(app_manager.RyuApp): 68 | 69 | OFP_VERSIONS = [ofproto_v1_0.OFP_VERSION, 70 | ofproto_v1_2.OFP_VERSION, 71 | ofproto_v1_3.OFP_VERSION] 72 | 73 | _CONTEXTS = {'dpset': dpset.DPSet} 74 | 75 | def __init__(self, *args, **kwargs): 76 | super(ZeekController, self).__init__(*args, **kwargs); 77 | 78 | self.dpset = kwargs['dpset'] 79 | self.data = {} 80 | self.data['dpset'] = self.dpset 81 | # store DPID->name mapping. We create the mapping implicitely 82 | # for all flow_clear and flow_mod events that we get and use 83 | # it for the returning flow_removed events 84 | self.dpids = {} 85 | 86 | self.epl = broker.Endpoint() 87 | 88 | def start(self): 89 | self.epl.listen("127.0.0.1", 9999) 90 | self.status_subscriber = self.epl.make_status_subscriber(True) 91 | self.subscriber = self.epl.make_subscriber(queuename) 92 | 93 | self.threads.append(hub.spawn(self._broker_loop)) 94 | self.logger.info("Started broker communication...") 95 | self.threads.append(hub.spawn(self._event_loop)) 96 | 97 | @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) 98 | def _switch_features_handler(self, ev): 99 | dp = ev.msg.datapath 100 | # here we could alarm that we have seen a new switch 101 | 102 | def _broker_loop(self): 103 | self.logger.info("Broker loop...") 104 | 105 | while 1==1: 106 | self.logger.info("Waiting for broker message") 107 | readable, writable, exceptional = select( 108 | [self.status_subscriber.fd(), self.subscriber.fd()], 109 | [],[]) 110 | 111 | if ( self.status_subscriber.fd() in readable ): 112 | self.logger.info("Got broker status message") 113 | msg = self.status_subscriber.get() 114 | self.handle_broker_message(msg) 115 | elif ( self.subscriber.fd() in readable ): 116 | self.logger.info("Got broker message") 117 | msg = self.subscriber.get() 118 | self.handle_broker_message(msg) 119 | 120 | def handle_broker_message(self, m): 121 | if isinstance(m, broker.Status): 122 | if m.code() == broker.SC.PeerAdded: 123 | self.logger.info("Incoming connection established.") 124 | return 125 | 126 | return 127 | 128 | if ( type(m).__name__ != "tuple" ): 129 | self.logger.error("Unexpected type %s, expected tuple", type(m).__name__) 130 | return 131 | 132 | if ( len(m) < 1 ): 133 | self.logger.error("Tuple without content?") 134 | return 135 | 136 | (topic, event) = m 137 | ev = broker.zeek.Event(event) 138 | event_name = ev.name() 139 | 140 | if ( event_name == "OpenFlow::broker_flow_clear" ): 141 | self.event_flow_clear(ev.args()) 142 | elif ( event_name == "OpenFlow::broker_flow_mod" ): 143 | self.event_flow_mod(ev.args()) 144 | elif event_name == "OpenFlow::flow_mod_success": 145 | pass 146 | elif event_name == "OpenFlow::flow_mod_failure": 147 | pass 148 | elif event_name == "OpenFlow::flow_removed": 149 | pass 150 | else: 151 | self.logger.error("Unknown event %s", event_name) 152 | return 153 | 154 | def event_flow_clear(self, m): 155 | if ( len(m) != 2 ) or ( not isinstance(m[0], str) ) or ( not isinstance(m[1], broker.Count) ): 156 | self.logger.error("wrong number of elements or type in tuple for event_flow_clear") 157 | return 158 | 159 | # since this is only a convenience function we should return it and just do the 160 | # flow-mod from Zeek ourselves 161 | name = m[0] 162 | 163 | dpid = m[1].value 164 | self.logger.info("flow_clear for %s %d", name, dpid) 165 | 166 | dp = ryu.app.ofctl.api.get_datapath(self, int(dpid)) 167 | 168 | if dp is None: 169 | self.logger.error("dpid %d not found for clear", dpid) 170 | return 171 | 172 | self.dpids[dp.id] = name 173 | 174 | flow = {'table_id': dp.ofproto.OFPTT_ALL} 175 | _ofp_version = dp.ofproto.OFP_VERSION 176 | _ofctl = supported_ofctl.get(_ofp_version, None) 177 | if _ofctl is None: 178 | self.logger.error("unsupported openflow protocol") 179 | return 180 | 181 | dp.brosend = 1 # give it to us 182 | _ofctl.mod_flow_entry(dp, flow, dp.ofproto.OFPFC_DELETE) 183 | msg = dp.bromessage 184 | 185 | ryu.app.ofctl.api.send_msg(self, msg) 186 | 187 | def send_error(self, name, match, flow_mod, msg): 188 | ev = broker.zeek.Event("OpenFlow::flow_mod_failure", name, match, flow_mod, msg) 189 | self.epl.publish(queuename, ev) 190 | 191 | def send_success(self, name, match, flow_mod, msg): 192 | ev = broker.zeek.Event("OpenFlow::flow_mod_success", name, match, flow_mod, msg) 193 | self.epl.publish(queuename, ev) 194 | 195 | def event_flow_mod(self, m): 196 | if ( len(m) != 4 ) or ( not isinstance(m[0], str) ) or ( not isinstance(m[1], broker.Count) ) or ( not isinstance(m[2], tuple) ) or ( not isinstance(m[3], tuple) ): 197 | self.logger.error("wrong number of elements or type in tuple for event_flow_mod") 198 | return 199 | 200 | name = m[0] 201 | 202 | dpid = m[1].value 203 | match = self.parse_ofp_match(m[2]) 204 | flow_mod = self.parse_ofp_flow_mod(m[3]) 205 | 206 | dp = ryu.app.ofctl.api.get_datapath(self, int(dpid)) 207 | 208 | if dp is None: 209 | self.logger.error("name %s dpid %d not found for flow_mod", name, dpid) 210 | self.send_error(name, m[2], m[3], "dpid not found") 211 | return 212 | 213 | self.dpids[dp.id] = name 214 | 215 | if dp.ofproto.OFP_VERSION != ofproto_v1_0.OFP_VERSION: 216 | if 'nw_dst' in match: 217 | if ":" in match['nw_dst']: 218 | match['ipv6_dst'] = match['nw_dst'] 219 | else: 220 | match['ipv4_dst'] = match['nw_dst'] 221 | del match['nw_dst'] 222 | 223 | if 'nw_src' in match: 224 | if ":" in match['nw_src']: 225 | match['ipv6_src'] = match['nw_src'] 226 | else: 227 | match['ipv4_src'] = match['nw_src'] 228 | del match['nw_src'] 229 | 230 | if 'tp_src' in match: 231 | proto = match.get('nw_proto', None); 232 | if proto == None: 233 | self.logger.error("Cannot determine proto for flow mod") 234 | return 235 | 236 | if proto == 0x06: 237 | match['tcp_src'] = match['tp_src'] 238 | del match['tp_src'] 239 | elif proto == 0x11: 240 | match['udp_src'] = match['tp_src'] 241 | del match['tp_src'] 242 | elif proto == 0x01: 243 | match['icmpv4_type'] = match['tp_src'] 244 | del match['tp_src'] 245 | 246 | 247 | if 'tp_dst' in match: 248 | proto = match.get('nw_proto', None); 249 | if proto == None: 250 | self.logger.error("Cannot determine proto for flow mod") 251 | return 252 | 253 | if proto == 0x06: 254 | match['tcp_dst'] = match['tp_dst'] 255 | del match['tp_dst'] 256 | elif proto == 0x11: 257 | match['udp_dst'] = match['tp_dst'] 258 | del match['tp_dst'] 259 | elif proto == 0x01: 260 | match['icmpv4_code'] = match['tp_dst'] 261 | del match['tp_dst'] 262 | 263 | self.logger.info("flow_mod for %d", dpid) 264 | #print match 265 | #print flow_mod 266 | 267 | 268 | cmdstr = flow_mod['command'] 269 | cmd = self.string_to_command(dp, cmdstr) 270 | 271 | flow_mod['match'] = match 272 | #flow_mod['flags'] = 1 # remove, we actually want overlapping entries sometimes. 273 | actions = [] 274 | 275 | if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: 276 | for k, v in flow_mod['actions'].iteritems(): 277 | if k == 'vlan_vid': 278 | actions.append(dp.ofproto_parser.OFPActionVlanVid(v)) 279 | elif k == 'vlan_pcp': 280 | actions.append(dp.ofproto_parser.OFPActionVlanPcp(k)) 281 | elif k == 'vlan_strip' and v == True: 282 | actions.append(dp.ofproto_parser.OFPActionStripVlan()) 283 | elif k == 'dl_src': 284 | dl_src = haddr_to_bin(v) 285 | actions.append(dp.ofproto_parser.OFPActionSetDlSrc(dl_src)) 286 | elif k == 'dl_dst': 287 | dl_dst = haddr_to_bin(v) 288 | actions.append(dp.ofproto_parser.OFPActionSetDlDst(dl_dst)) 289 | elif k == 'nw_tos': 290 | actions.append(dp.ofproto_parser.OFPActionSetNwTos(v)) 291 | elif k == 'nw_src': 292 | actions.append(dp.ofproto_parser.OFPActionSetNwSrc(ipv4_to_int(v))) 293 | elif k == 'nw_dst': 294 | actions.append(dp.ofproto_parser.OFPActionSetNwDst(ipv4_to_int(v))) 295 | elif k == 'tp_src': 296 | actions.append(dp.ofproto_parser.OFPActionSetTpSrc(v)) 297 | elif k == 'tp_dst': 298 | actions.append(dp.ofproto_parser.OFPActionSetTpDst(v)) 299 | else: 300 | 301 | if 'nw_dst' in flow_mod['actions']: 302 | if ":" in flow_mod['actions']['nw_dst']: 303 | flow_mod['actions']['ipv6_dst'] = flow_mod['actions']['nw_dst'] 304 | else: 305 | flow_mod['actions']['ipv4_dst'] = flow_mod['actions']['nw_dst'] 306 | del flow_mod['actions']['nw_dst'] 307 | 308 | if 'nw_src' in flow_mod['actions']: 309 | if ":" in flow_mod['actions']['nw_src']: 310 | flow_mod['actions']['ipv6_src'] = flow_mod['actions']['nw_src'] 311 | else: 312 | flow_mod['actions']['ipv4_src'] = flow_mod['actions']['nw_src'] 313 | del flow_mod['actions']['nw_src'] 314 | 315 | if 'tp_src' in flow_mod['actions']: 316 | proto = match.get('nw_proto', None); 317 | if proto == None: 318 | self.logger.error("Cannot determine proto for flow mod") 319 | return 320 | 321 | if proto == 0x06: 322 | flow_mod['actions']['tcp_src'] = flow_mod['actions']['tp_src'] 323 | elif proto == 0x11: 324 | flow_mod['actions']['udp_src'] = flow_mod['actions']['tp_src'] 325 | elif proto == 0x01: 326 | flow_mod['actions']['icmpv4_type'] = flow_mod['actions']['tp_src'] 327 | 328 | if 'tp_dst' in flow_mod['actions']: 329 | proto = match.get('nw_proto', None); 330 | if proto == None: 331 | self.logger.error("Cannot determine proto for flow mod") 332 | return 333 | 334 | if proto == 0x06: 335 | flow_mod['actions']['tcp_dst'] = flow_mod['actions']['tp_dst'] 336 | elif proto == 0x11: 337 | flow_mod['actions']['udp_dst'] = flow_mod['actions']['tp_dst'] 338 | elif proto == 0x01: 339 | flow_mod['actions']['icmpv4_code'] = flow_mod['actions']['tp_dst'] 340 | 341 | for k, v in flow_mod['actions'].iteritems(): 342 | if k == 'vlan_strip' and v == True: 343 | actions.append(dp.ofproto_parser.OFPActionStripVlan()) 344 | elif ( k == 'vlan_vid' ) or ( k == 'vlan_pcp' ) or ( k == 'nw_tos' ) or ( k == 'ipv4_src' ) or ( k == 'ipv4_dst' ) or ( k == 'tcp_src' ) or ( k == 'tcp_dst' ) or ( k == 'udp_src' ) or ( k == 'udp_dst' ) or ( k == 'icmpv4_code' ) or ( k == 'icmpv4_type' ) or ( k == 'ipv6_src' ) or ( k == 'ipv6_dst' ): 345 | pass 346 | actions.append(dp.ofproto_parser.OFPActionSetField(**{k: v})) 347 | elif ( k == 'dl_src' ) or ( k == 'dl_dst' ): 348 | #dl = haddr_to_bin(v) 349 | actions.append(dp.ofproto_parser.OFPActionSetField(**{k: v})) 350 | 351 | # do out-ports separately because it has to be last... 352 | if 'out_ports' in flow_mod['actions']: 353 | for i in flow_mod['actions']['out_ports']: 354 | max_len = 0xffe5 355 | if dp.ofproto.OFP_VERSION != ofproto_v1_0.OFP_VERSION: 356 | max_len = dp.ofproto.OFPCML_MAX 357 | 358 | if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: 359 | if i == 0xfffffff8: 360 | i = dp.ofproto.OFPP_IN_PORT 361 | elif i == 0xfffffff9: 362 | i = dp.ofproto.OFPP_TABLE 363 | elif i == 0xfffffffa: 364 | i = dp.ofproto.OFPP_NORMAL 365 | elif i == 0xfffffffb: 366 | i = dp.ofproto.OFPP_FLOOD 367 | elif i == 0xfffffffc: 368 | i = dp.ofproto.OFPP_ALL 369 | elif i == 0xfffffffd: 370 | i = dp.ofproto.OFPP_CONTROLLER 371 | elif i == 0xfffffffe: 372 | i = dp.ofproto.OFPP_LOCAL 373 | elif i == 0xffffffff: 374 | i = dp.ofproto.OFPP_ANY 375 | 376 | actions.append(dp.ofproto_parser.OFPActionOutput(i, max_len)) 377 | 378 | del flow_mod['actions'] 379 | 380 | if cmd is None: 381 | self.logger.error("command %s could not be parsed", cmdstr) 382 | self.send_error(m[2], m[3], "cmd not recognized") 383 | return 384 | 385 | _ofp_version = dp.ofproto.OFP_VERSION 386 | _ofctl = supported_ofctl.get(_ofp_version, None) 387 | if _ofctl is None: 388 | self.logger.error("unsupported openflow protocol") 389 | self.send_error(m[2], m[3], "unsupported openflow protocol") 390 | return 391 | 392 | dp.brosend = 1 # give it to us... 393 | _ofctl.mod_flow_entry(dp, flow_mod, cmd) 394 | msg = dp.bromessage 395 | 396 | # naming and calling changed in the api for of1.0 vs 1.3 397 | 398 | insts = [] 399 | if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: 400 | msg.actions = actions 401 | else: 402 | insts.append(dp.ofproto_parser.OFPInstructionActions(dp.ofproto.OFPIT_APPLY_ACTIONS, actions)) 403 | msg.instructions = insts 404 | 405 | #print "Sending to switch:" 406 | #print msg 407 | 408 | try: 409 | ryu.app.ofctl.api.send_msg(self, msg) 410 | self.send_success(name, m[2], m[3], "") 411 | except ryu.app.ofctl.exception.OFError as err: 412 | self.logger.error("flow_mod execution error %s", err) 413 | self.send_error(name, m[2], m[3], str(err)) 414 | 415 | def parse_ofp_match(self, m): 416 | match = ['in_port', 'dl_src', 'dl_dst', 'dl_vlan', 'dl_vlan_pcp', 'dl_type', 'nw_tos', 'nw_proto', 'nw_src', 'nw_dst', 'tp_src', 'tp_dst'] 417 | return self.record_to_record(match, m) 418 | 419 | 420 | def parse_ofp_flow_mod(self, m): 421 | match = ['cookie', 'table_id', 'command', 'idle_timeout', 'hard_timeout', 'priority', 'out_port', 'out_group', 'flags'] 422 | 423 | rec = self.record_to_record(match, m) 424 | 425 | # ok, now we have to get the actions, which are after flags. This is kind of cheating, but... whatever :) 426 | match_actions = ['out_ports', 'vlan_vid', 'vlan_pcp', 'vlan_strip', 'dl_src', 'dl_dst', 'nw_tos', 'nw_src', 'nw_dst', 'tp_src', 'tp_dst'] 427 | 428 | rm = m 429 | rl = rm[9] 430 | recaction = self.record_to_record(match_actions, rl) 431 | rec['actions'] = recaction 432 | 433 | return rec 434 | 435 | @set_ev_cls(ofp_event.EventOFPFlowRemoved, MAIN_DISPATCHER) 436 | def _flow_removed_handler(self, ev): 437 | msg = ev.msg 438 | dp = msg.datapath 439 | ofp = dp.ofproto 440 | match = msg.match 441 | 442 | if dp.id not in self.dpids: 443 | self.logger.error("Flow remove for unknown DPID %d", dp.id) 444 | return 445 | 446 | #print "Flow removed" 447 | 448 | match_vec = vector_of_field([]) 449 | match_vec = self.vec_add_field(match_vec, match, 'in_port'); 450 | match_vec = self.vec_add_field(match_vec, match, 'eth_src'); 451 | match_vec = self.vec_add_field(match_vec, match, 'eth_dst'); 452 | match_vec = self.vec_add_field(match_vec, match, 'vlan_vid'); 453 | match_vec = self.vec_add_field(match_vec, match, 'vlan_pcp'); 454 | match_vec = self.vec_add_field(match_vec, match, 'eth_type'); 455 | match_vec = self.vec_add_field(match_vec, match, 'ip_dscp'); 456 | match_vec = self.vec_add_field(match_vec, match, 'ip_proto'); 457 | 458 | src = match.get('ipv4_src', match.get('ipv6_src', match.get('nw_src', None))) 459 | dst = match.get('ipv4_dst', match.get('ipv6_dst', match.get('nw_dst', None))) 460 | 461 | if src != None: 462 | sn = None 463 | if ( not isinstance(src, tuple) ) and( ":" in src ): 464 | sn = subnet(address.from_string(src), 128) 465 | elif not isinstance(src, tuple): 466 | sn = subnet(address.from_string(src), 32) 467 | else: 468 | adr = address.from_string(src[0]) 469 | ip = IPNetwork(src[0]+"/"+src[1]) 470 | sn = subnet(adr, ip.prefixlen) 471 | match_vec.push_back(field(data(sn))) 472 | else: 473 | match_vec.push_back(field()) 474 | 475 | if dst != None: 476 | sn = None 477 | if ( not isinstance(dst, tuple) ) and( ":" in dst ): 478 | sn = subnet(address.from_string(dst), 128) 479 | elif not isinstance(dst, tuple): 480 | sn = subnet(address.from_string(dst), 32) 481 | else: 482 | adr = address.from_string(dst[0]) 483 | ip = IPNetwork(dst[0]+"/"+dst[1]) 484 | sn = subnet(adr, ip.prefixlen) 485 | match_vec.push_back(field(data(sn))) 486 | else: 487 | match_vec.push_back(field()) 488 | 489 | srcp = match.get('tcp_src', match.get('udp_src', match.get('icmpv4_type', match.get('tp_src', None)))) 490 | dstp = match.get('tcp_dst', match.get('udp_dst', match.get('icmpv4_type', match.get('tp_dst', None)))) 491 | 492 | if srcp != None: 493 | match_vec.push_back(field(data(srcp))) 494 | else: 495 | match_vec.push_back(field()) 496 | 497 | if dstp != None: 498 | match_vec.push_back(field(data(dstp))) 499 | else: 500 | match_vec.push_back(field()) 501 | 502 | args = [self.dpids[dp.id], 503 | match_vec, 504 | broker.Count(msg.cookie), 505 | broker.Count(msg.priority), 506 | broker.Count(msg.reason), 507 | broker.Count(msg.duration_sec), 508 | broker.Count(msg.idle_timeout), 509 | broker.Count(msg.packet_count), 510 | broker.Count(msg.byte_count)] 511 | ev = broker.zeek.Event("OpenFlow::flow_removed", args) 512 | self.epl.publish(queuename, ev) 513 | 514 | 515 | def vec_add_field(self, match_vec, match , el): 516 | if el in match: 517 | match_vec.push_back(field(data(match[el]))) 518 | else: 519 | match_vec.push_back(field()) 520 | 521 | return match_vec 522 | 523 | def string_to_command(self, dp, cmdstr): 524 | if ( cmdstr == "OFPFC_ADD" ): 525 | return dp.ofproto.OFPFC_ADD 526 | elif ( cmdstr == "OFPFC_MODIFY" ): 527 | return dp.ofproto.OFPFC_MODIFY 528 | elif ( cmdstr == "OFPFC_MODIFY_STRICT" ): 529 | return dp.ofproto.OFPFC_MODIFY_STRICT 530 | elif ( cmdstr == "OFPFC_DELETE" ): 531 | return dp.ofproto.OFPFC_DELETE 532 | elif ( cmdstr == "OFPFC_DELETE_STRICT" ): 533 | return dp.ofproto.OFPFC_DELETE_STRICT 534 | else: 535 | return None 536 | 537 | def record_to_record(self, match, m): 538 | #if len(match) != len(m): 539 | # self.logger.error("wrong number of elements in parse_ofp_match") 540 | # return 541 | 542 | if not isinstance(m, tuple): 543 | self.logger.error("Got non record element") 544 | 545 | rec = m 546 | 547 | dict = {} 548 | for i in range(0, len(match)): 549 | if rec[i] is None: 550 | #dict[match[i]] = None # most of the functions expect this to be undefined, not none. We oblige. 551 | continue 552 | 553 | dict[match[i]] = self.convert_element(rec[i]) 554 | 555 | return dict 556 | 557 | def convert_element(self, el): 558 | if isinstance(el, broker.Count): 559 | return el.value 560 | 561 | if isinstance(el, ipaddress.IPv4Address): 562 | return str(el); 563 | 564 | if isinstance(el, ipaddress.IPv6Address): 565 | return str(el); 566 | 567 | if isinstance(el, ipaddress.IPv4Network): 568 | return str(el); 569 | 570 | if isinstance(el, ipaddress.IPv6Network): 571 | return str(el); 572 | 573 | if isinstance(el, broker.Port): 574 | p = str(el) 575 | ex = re.compile('([0-9]+)(.*)') 576 | res = ex.match(p) 577 | return (res.group(1), res.group(2)) 578 | 579 | if isinstance(el, broker.Enum): 580 | tmp = el.name 581 | return re.sub(r'.*::', r'', tmp) 582 | 583 | if isinstance(el, tuple): 584 | return tuple(self.convert_element(ell) for ell in el); 585 | 586 | if isinstance(el, datetime.datetime): 587 | return el 588 | 589 | if isinstance(el, datetime.timedelta): 590 | return el 591 | 592 | if isinstance(el, int): 593 | return el 594 | 595 | if isinstance(el, str): 596 | return el 597 | 598 | logger.error("Unsupported type %s", type(el) ) 599 | return el; 600 | -------------------------------------------------------------------------------- /openflow/example.zeek: -------------------------------------------------------------------------------- 1 | @load base/protocols/conn 2 | @load base/frameworks/openflow 3 | @load base/frameworks/netcontrol 4 | 5 | const broker_port: port = 9999/tcp &redef; 6 | global of_controller: OpenFlow::Controller; 7 | 8 | # Switch datapath ID 9 | const switch_dpid: count = 12 &redef; 10 | # port on which Zeek is listening - we install a rule to the switch to mirror traffic here... 11 | const switch_bro_port: count = 19 &redef; 12 | 13 | 14 | event NetControl::init() &priority=2 15 | { 16 | of_controller = OpenFlow::broker_new("of", 127.0.0.1, broker_port, "zeek/openflow", switch_dpid); 17 | local pacf_of = NetControl::create_openflow(of_controller, NetControl::OfConfig($monitor=T, $forward=F, $priority_offset=+5)); 18 | NetControl::activate(pacf_of, 0); 19 | } 20 | 21 | event Broker::peer_added(endpoint: Broker::EndpointInfo, msg: string) 22 | { 23 | print "Broker peer added", endpoint$network; 24 | } 25 | 26 | event NetControl::init_done() 27 | { 28 | print "NeControl is starting operations"; 29 | OpenFlow::flow_clear(of_controller); 30 | OpenFlow::flow_mod(of_controller, [], [$cookie=OpenFlow::generate_cookie(1337), $priority=2, $command=OpenFlow::OFPFC_ADD, $actions=[$out_ports=vector(switch_bro_port)]]); 31 | } 32 | 33 | event NetControl::rule_added(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 34 | { 35 | print "Rule added successfully", r$id; 36 | } 37 | 38 | event NetControl::rule_error(r: NetControl::Rule, p: NetControl::PluginState, msg: string) 39 | { 40 | print "Rule error", r$id, msg; 41 | } 42 | 43 | event NetControl::rule_timeout(r: NetControl::Rule, i: NetControl::FlowInfo, p: NetControl::PluginState) 44 | { 45 | print "Rule timeout", r$id, i; 46 | } 47 | 48 | event OpenFlow::flow_mod_success(name: string, match: OpenFlow::ofp_match, flow_mod: OpenFlow::ofp_flow_mod, msg: string) 49 | { 50 | #print "Flow mod success"; 51 | } 52 | 53 | event OpenFlow::flow_mod_failure(name: string, match: OpenFlow::ofp_match, flow_mod: OpenFlow::ofp_flow_mod, msg: string) 54 | { 55 | print "Flow mod failure", flow_mod$cookie, msg; 56 | } 57 | 58 | event OpenFlow::flow_removed(name: string, match: OpenFlow::ofp_match, cookie: count, priority: count, reason: count, duration_sec: count, idle_timeout: count, packet_count: count, byte_count: count) 59 | { 60 | print "Flow removed", match; 61 | } 62 | 63 | # Shunt all ssl, gridftp and ssh connections after we cannot get any data from them anymore 64 | 65 | event ssl_established(c: connection) 66 | { 67 | local id = c$id; 68 | NetControl::shunt_flow([$src_h=id$orig_h, $src_p=id$orig_p, $dst_h=id$resp_h, $dst_p=id$resp_p], 30sec); 69 | } 70 | 71 | event GridFTP::data_channel_detected(c: connection) 72 | { 73 | local id = c$id; 74 | NetControl::shunt_flow([$src_h=id$orig_h, $src_p=id$orig_p, $dst_h=id$resp_h, $dst_p=id$resp_p], 30sec); 75 | } 76 | 77 | event ssh_auth_successful(c: connection, auth_method_none: bool) 78 | { 79 | if ( ! c$ssh$auth_success ) 80 | return; 81 | 82 | local id = c$id; 83 | NetControl::shunt_flow([$src_h=id$orig_h, $src_p=id$orig_p, $dst_h=id$resp_h, $dst_p=id$resp_p], 5sec); 84 | print current_time(); 85 | } 86 | -------------------------------------------------------------------------------- /test/simple-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Simple command line client using the provided API. Test, e.g., with 4 | # the provided simple-test.zeek 5 | 6 | import logging, netcontrol, pprint 7 | 8 | logging.basicConfig(level=logging.DEBUG) 9 | 10 | ep = netcontrol.Endpoint("zeek/event/netcontrol-example", "127.0.0.1", 9977); 11 | pp = pprint.PrettyPrinter(indent=4) 12 | 13 | while 1==1: 14 | response = ep.getNextCommand() 15 | 16 | if response.type == netcontrol.ResponseType.AddRule: 17 | ep.sendRuleAdded(response, "") 18 | elif response.type == netcontrol.ResponseType.RemoveRule: 19 | ep.sendRuleRemoved(response, "") 20 | else: 21 | continue 22 | 23 | pp.pprint(response.rule) 24 | -------------------------------------------------------------------------------- /test/simple-test.zeek: -------------------------------------------------------------------------------- 1 | @load base/frameworks/netcontrol 2 | 3 | redef exit_only_after_terminate = T; 4 | 5 | event NetControl::init() 6 | { 7 | local netcontrol_broker = NetControl::create_broker(NetControl::BrokerConfig($host=127.0.0.1, $bport=9977/tcp, $topic="zeek/event/netcontrol-example"), T); 8 | NetControl::activate(netcontrol_broker, 0); 9 | } 10 | 11 | function test_mac_flow() 12 | { 13 | local flow = NetControl::Flow( 14 | $src_m = "FF:FF:FF:FF:FF:FF" 15 | ); 16 | local e: NetControl::Entity = [$ty=NetControl::FLOW, $flow=flow]; 17 | local r: NetControl::Rule = [$ty=NetControl::DROP, $target=NetControl::FORWARD, $entity=e, $expire=15sec]; 18 | 19 | NetControl::add_rule(r); 20 | } 21 | 22 | function test_mac() 23 | { 24 | local e: NetControl::Entity = [$ty=NetControl::MAC, $mac="FF:FF:FF:FF:FF:FF"]; 25 | local r: NetControl::Rule = [$ty=NetControl::DROP, $target=NetControl::FORWARD, $entity=e, $expire=15sec]; 26 | 27 | NetControl::add_rule(r); 28 | } 29 | 30 | event NetControl::init_done() &priority=-5 31 | { 32 | print "Init done"; 33 | NetControl::shunt_flow([$src_h=192.168.17.1, $src_p=32/tcp, $dst_h=192.168.17.2, $dst_p=32/tcp], 30sec); 34 | NetControl::drop_address(1.1.2.2, 15sec, "Hi there"); 35 | NetControl::whitelist_address(1.2.3.4, 15sec); 36 | NetControl::redirect_flow([$src_h=192.168.17.1, $src_p=32/tcp, $dst_h=192.168.17.2, $dst_p=32/tcp], 5, 30sec); 37 | NetControl::quarantine_host(127.0.0.2, 8.8.8.8, 127.0.0.3, 15sec); 38 | test_mac(); 39 | test_mac_flow(); 40 | } 41 | 42 | --------------------------------------------------------------------------------