├── .gitignore ├── LICENSE.txt ├── README.md ├── bin ├── hookswitch-example-controller ├── hookswitch-nfq └── hookswitch-of13 ├── hookswitch ├── __init__.py ├── cli │ ├── __init__.py │ ├── example_controller.py │ ├── nfq.py │ └── of13.py ├── internal │ ├── __init__.py │ ├── common.py │ ├── nfq.py │ └── zmqclient.py ├── nfq.py └── openflow.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | ### JetBrains template 62 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 63 | 64 | *.iml 65 | 66 | ## Directory-based project format: 67 | .idea/ 68 | # if you remove the above rule, at least ignore the following: 69 | 70 | # User-specific stuff: 71 | # .idea/workspace.xml 72 | # .idea/tasks.xml 73 | # .idea/dictionaries 74 | 75 | # Sensitive or high-churn files: 76 | # .idea/dataSources.ids 77 | # .idea/dataSources.xml 78 | # .idea/sqlDataSources.xml 79 | # .idea/dynamic.xml 80 | # .idea/uiDesigner.xml 81 | 82 | # Gradle: 83 | # .idea/gradle.xml 84 | # .idea/libraries 85 | 86 | # Mongo Explorer plugin: 87 | # .idea/mongoSettings.xml 88 | 89 | ## File-based project format: 90 | *.ipr 91 | *.iws 92 | 93 | ## Plugin-specific files: 94 | 95 | # IntelliJ 96 | /out/ 97 | 98 | # mpeltonen/sbt-idea plugin 99 | .idea_modules/ 100 | 101 | # JIRA plugin 102 | atlassian-ide-plugin.xml 103 | 104 | # Crashlytics plugin (for Android Studio and IntelliJ) 105 | com_crashlytics_export_strings.xml 106 | crashlytics.properties 107 | crashlytics-build.properties 108 | 109 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HookSwitch: A Usermode Packet Injection Library 2 | 3 | [![PyPI version](https://badge.fury.io/py/hookswitch.svg)](http://badge.fury.io/py/hookswitch) 4 | 5 | ## Possible Recipes 6 | 7 | * Fault Injection (Example: [Namazu](https://github.com/osrg/namazu)) 8 | * L7-aware firewall (Note that you might not get good performance. However, it's still useful for prototyping.) 9 | 10 | and so on.. 11 | 12 | HookSwitch was originally developed for [Namazu](https://github.com/osrg/namazu), but we believe HookSwitch can be also used for other purposes. 13 | 14 | ## Supported Backends 15 | 16 | * Openflow 1.3 compliant switches 17 | * Linux netfilter queue (effective for loopback interfaces) 18 | 19 | ## Install 20 | For Python 2: 21 | 22 | $ sudo pip install hookswitch 23 | 24 | For Python 3 [NOT YET SUPPORTED]: 25 | 26 | $ sudo pip3 install hookswitch 27 | 28 | 29 | ## Usage (Openflow 1.3 implementation) 30 | In this section, we suppose you have already set up Openflow switch (e.g. OVS) and Ryu Framework. 31 | 32 | $ hookswitch-example-controller ipc:///tmp/hookswitch-socket & 33 | $ hookswitch-of13 ipc:///tmp/hookswitch-socket --tcp-ports=4242,4243,4244 34 | 35 | 36 | ## Usage (Linux netfilter queue implementation) 37 | 38 | $ sudo iptables -A OUTPUT -p tcp -m owner --uid-owner johndoe -j NFQUEUE --queue-num 42 39 | $ hookswitch-example-controller ipc:///tmp/hookswitch-socket & 40 | $ sudo hookswitch-nfq ipc:///tmp/hookswitch-socket --nfq-number=42 41 | 42 | 43 | ## API Design 44 | HookSwitch works as a ZeroMQ client. 45 | 46 | You can implement your application ("Controller") as a ZeroMQ server in an arbitrary language. 47 | 48 | ZeroMQ message format: 49 | 50 | +------------------------------+ 51 | | JSON metadata | 52 | +------------------------------+ 53 | | Ethernet Frame | 54 | +------------------------------+ 55 | 56 | NOTE: In Linux netfilter queue implementation, Ethernet header is always like this: 57 | 58 | FF FF FF FF FF FF 00 00 00 00 00 00 08 00 59 | 60 | ### JSON Metadata 61 | 62 | HookSwitch -> Controller: 63 | 64 | - `id`(int): Ethernet frame ID 65 | 66 | HookSwitch <- Controller: 67 | 68 | - `id`(int): Ethernet frame ID 69 | - `op`(string): either one of {`accept`, `drop`, `modify`}. If `op` is not `modify`, the Ethernet frame *must* be ignored. 70 | 71 | 72 | ## Related Projects 73 | * [Namazu (Earthquake)](https://github.com/osrg/namazu) 74 | * [HookFS](https://github.com/osrg/hookfs) 75 | 76 | ## How to Contribute 77 | We welcome your contribution to HookSwitch. 78 | Please feel free to send your pull requests on github! 79 | 80 | ## Copyright 81 | Copyright (C) 2015 [Nippon Telegraph and Telephone Corporation](http://www.ntt.co.jp/index_e.html). 82 | 83 | Released under [Apache License 2.0](LICENSE). 84 | -------------------------------------------------------------------------------- /bin/hookswitch-example-controller: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec python2 -m hookswitch.cli.example_controller "$@" 3 | -------------------------------------------------------------------------------- /bin/hookswitch-nfq: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec python2 -m hookswitch.cli.nfq "$@" 3 | -------------------------------------------------------------------------------- /bin/hookswitch-of13: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for i in "$@" 3 | do 4 | case $i in 5 | --tcp-ports=*) 6 | export HOOKSWITCH_TCP_PORTS="${i#*=}" 7 | shift 8 | ;; 9 | --udp-ports=*) 10 | export HOOKSWITCH_UDP_PORTS="${i#*=}" 11 | shift 12 | ;; 13 | --debug) 14 | export HOOKSWITCH_DEBUG=1 15 | shift 16 | ;; 17 | *) 18 | export HOOKSWITCH_ZMQ_ADDR="${i#*=}" 19 | ;; 20 | esac 21 | done 22 | 23 | # TODO: support python3 ryu-manager 24 | exec python2 $(which ryu-manager) hookswitch.cli.of13 25 | -------------------------------------------------------------------------------- /hookswitch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osrg/hookswitch/cde8f136b8820c8e22ddc48b9234968beec18ec8/hookswitch/__init__.py -------------------------------------------------------------------------------- /hookswitch/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osrg/hookswitch/cde8f136b8820c8e22ddc48b9234968beec18ec8/hookswitch/cli/__init__.py -------------------------------------------------------------------------------- /hookswitch/cli/example_controller.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import hexdump 3 | import json 4 | import logging 5 | import eventlet 6 | from eventlet.green import zmq 7 | 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser( 11 | description='HookSwitch Controller Example') 12 | help_str = 'ZeroMQ bind address (e.g. ipc:///tmp/hookswitch-socket)' 13 | parser.add_argument('ZMQ_ADDR', 14 | type=str, 15 | help=help_str) 16 | args = parser.parse_args() 17 | worker = Worker(args.ZMQ_ADDR) 18 | print('Starting for %s' % (args.ZMQ_ADDR)) 19 | worker.start() 20 | 21 | 22 | class Worker(object): 23 | 24 | def __init__(self, zmq_addr): 25 | self.zmq_addr = zmq_addr 26 | 27 | def start(self): 28 | self.zmq_ctx = zmq.Context() 29 | self.zs = self.zmq_ctx.socket(zmq.PAIR) 30 | self.zs.bind(self.zmq_addr) 31 | handle = eventlet.spawn(self._zmq_worker) 32 | handle.wait() 33 | raise RuntimeError('should not reach here') 34 | 35 | def _zmq_worker(self): 36 | while True: 37 | metadata_str, eth_bytes = self.zs.recv_multipart() 38 | metadata = json.loads(metadata_str) 39 | print('===== Packet: %s =====' % metadata) 40 | print('Ethernet Frame (%d bytes)' % len(eth_bytes)) 41 | hexdump.hexdump(eth_bytes) 42 | self._accept(metadata) 43 | 44 | def _accept(self, metadata): 45 | assert isinstance(metadata, dict) 46 | resp_metadata = metadata.copy() 47 | resp_metadata['op'] = 'accept' 48 | resp_metadata_str = json.dumps(resp_metadata) 49 | self.zs.send_multipart((resp_metadata_str, '')) 50 | 51 | if __name__ == '__main__': 52 | import sys 53 | sys.exit(main()) 54 | -------------------------------------------------------------------------------- /hookswitch/cli/nfq.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | from hookswitch.nfq import NFQHook 4 | from hookswitch.internal import LOG 5 | 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser( 9 | description='HookSwitch for Linux netfilter queue') 10 | help_str = 'ZeroMQ connect address (e.g. ipc:///tmp/hookswitch-socket)' 11 | parser.add_argument('ZMQ_ADDR', 12 | type=str, 13 | help=help_str) 14 | parser.add_argument('-n', '--nfq-number', 15 | required=True, 16 | type=int, 17 | help='netfilter queue number') 18 | parser.add_argument('--debug', 19 | action='store_true', 20 | help='print debug info') 21 | 22 | args = parser.parse_args() 23 | 24 | if not args.debug: 25 | LOG.setLevel(logging.INFO) 26 | 27 | hook = NFQHook(nfq_number=args.nfq_number, zmq_addr=args.ZMQ_ADDR) 28 | print('Starting for %s (NFQ %d)' % (args.ZMQ_ADDR, args.nfq_number)) 29 | hook.start() 30 | 31 | if __name__ == '__main__': 32 | import sys 33 | sys.exit(main()) 34 | -------------------------------------------------------------------------------- /hookswitch/cli/of13.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from hookswitch.openflow import OF13Hook 4 | from hookswitch.internal import LOG 5 | 6 | 7 | class _OF13Hook(OF13Hook): 8 | 9 | """ 10 | should be called from ryu-manager 11 | """ 12 | 13 | def get_args(self): 14 | zmq_addr = os.getenv('HOOKSWITCH_ZMQ_ADDR') 15 | tcp_ports_str = os.getenv('HOOKSWITCH_TCP_PORTS') 16 | udp_ports_str = os.getenv('HOOKSWITCH_UDP_PORTS') 17 | debug = os.getenv('HOOKSWITCH_DEBUG') 18 | tcp_ports = [] 19 | udp_ports = [] 20 | 21 | if not zmq_addr: 22 | raise RuntimeError('ZMQ address is not specified') 23 | 24 | if tcp_ports_str: 25 | try: 26 | tcp_ports = [int(x) for x in tcp_ports_str.split(',')] 27 | except ValueError as ve: 28 | raise RuntimeError('Bad tcp ports', ve) 29 | 30 | if udp_ports_str: 31 | try: 32 | udp_ports = [int(x) for x in udp_ports_str.split(',')] 33 | except ValueError as ve: 34 | raise RuntimeError('Bad udp ports', ve) 35 | 36 | if not tcp_ports and not udp_ports: 37 | raise RuntimeError('No port is specified') 38 | 39 | return {'zmq_addr': zmq_addr, 40 | 'tcp_ports': tcp_ports, 41 | 'udp_ports': udp_ports, 42 | 'debug': debug} 43 | 44 | def __init__(self, *ryu_args, **ryu_kwargs): 45 | args = self.get_args() 46 | if not args['debug']: 47 | LOG.setLevel(logging.INFO) 48 | LOG.debug('Args: %s', args) 49 | 50 | super(_OF13Hook, self).__init__(tcp_ports=args['tcp_ports'], 51 | udp_ports=args['udp_ports'], 52 | zmq_addr=args['zmq_addr'], 53 | *ryu_args, **ryu_kwargs) 54 | -------------------------------------------------------------------------------- /hookswitch/internal/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | assert sys.version_info < (3, 0), 'Python 3k is not supported yet' 3 | 4 | from . import common 5 | LOG = common.init_logger() 6 | -------------------------------------------------------------------------------- /hookswitch/internal/common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | 5 | def init_logger(): 6 | logger = logging.getLogger(__name__) 7 | logger.setLevel(logging.DEBUG) 8 | handler = logging.StreamHandler(sys.stderr) 9 | formatter = logging.Formatter( 10 | '[%(levelname)s]%(filename)s:%(lineno)d: %(message)s') 11 | handler.setFormatter(formatter) 12 | handler.setLevel(logging.DEBUG) 13 | logger.addHandler(handler) 14 | return logger 15 | -------------------------------------------------------------------------------- /hookswitch/internal/nfq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from socket import AF_INET, ntohl 4 | from ctypes import * 5 | import logging 6 | import prctl 7 | 8 | from hookswitch.internal import LOG as _LOG 9 | 10 | LOG = _LOG.getChild(__name__) 11 | 12 | 13 | class NFQ(object): 14 | 15 | """ 16 | low-level NFQ library 17 | TODO: check SegV 18 | """ 19 | NF_DROP = 0 20 | NF_ACCEPT = 1 21 | NF_STOLEN = 2 22 | NF_QUEUE = 3 23 | NF_REPEAT = 4 24 | NF_STOP = 5 25 | 26 | NFQNL_COPY_NONE = 0 27 | NFQNL_COPY_META = 1 28 | NFQNL_COPY_PACKET = 2 29 | 30 | class NFQNL_MSG_PACKET_HDR(Structure): 31 | _fields_ = [('packet_id', c_uint32), 32 | ('hw_protocol', c_uint16), 33 | ('hook', c_uint8)] 34 | 35 | dll = CDLL('libnetfilter_queue.so.1') 36 | dll.nfq_get_msg_packet_hdr.argtypes = [c_void_p] 37 | dll.nfq_get_msg_packet_hdr.restype = POINTER(NFQNL_MSG_PACKET_HDR) 38 | dll.nfq_get_payload.argtypes = [c_void_p, POINTER(POINTER(c_char))] 39 | dll.nfq_get_payload.restype = c_int 40 | 41 | CALLBACK_CFUNCTYPE = CFUNCTYPE(c_int, 42 | c_void_p, # qh 43 | c_void_p, # nfmsg 44 | c_void_p, # nfad 45 | c_void_p) # data 46 | 47 | def __init__(self, q_num, cb, cb_data=c_void_p(None)): 48 | self._check_cap() 49 | self.h = self._make_handle() 50 | self.qh = self._make_queue_handle(self.h, q_num, cb, cb_data) 51 | self.fd = self.dll.nfq_fd(self.h) 52 | 53 | @classmethod 54 | def _check_cap(cls): 55 | assert prctl.cap_permitted.net_admin, 'missing CAP_NET_ADMIN' 56 | assert prctl.cap_permitted.net_raw, 'missing CAP_NET_RAW' 57 | 58 | @classmethod 59 | def _make_handle(cls): 60 | LOG.debug('Calling nfq_open()') 61 | h = cls.dll.nfq_open() 62 | LOG.debug('Called nfq_open=%d', h) 63 | assert h, 'h=%d' % h 64 | LOG.debug('Calling nfq_unbind_pf(%d, AF_INET)', h) 65 | unbound = cls.dll.nfq_unbind_pf(h, AF_INET) 66 | LOG.debug('Called nfq_unbind_pf=%d', unbound) 67 | assert unbound >= 0, 'unbound=%d' % unbound 68 | LOG.debug('Calling nfq_bind_pf(%d, AF_INET)', h) 69 | bound = cls.dll.nfq_bind_pf(h, AF_INET) 70 | LOG.debug('Called nfq_bind_pf=%d', bound) 71 | assert bound >= 0, 'bound=%d' % bound 72 | return h 73 | 74 | @classmethod 75 | def _make_queue_handle(cls, h, q_num, cb, data): 76 | LOG.debug( 77 | 'Calling nfq_create_queue(%s, %s, %s, %s)', h, q_num, cb, data) 78 | qh = cls.dll.nfq_create_queue(h, q_num, cb, data) 79 | LOG.debug('Called nfq_create_queue=%d', qh) 80 | assert qh, 'qh=%d' % qh 81 | LOG.debug('Calling nfq_set_mode(%d, NFQNL_COPY_PACKET)', qh) 82 | mode_set = cls.dll.nfq_set_mode(qh, cls.NFQNL_COPY_PACKET) 83 | LOG.debug('Called nfq_set_mode=%d', mode_set) 84 | assert mode_set >= 0, 'mode_set=%d' % mode_set 85 | return qh 86 | 87 | def close(self): 88 | LOG.debug('Calling nfq_close(%d)', self.h) 89 | self.dll.nfq_close(self.h) 90 | LOG.debug('Called nfq_close()') 91 | 92 | def handle_packet(self, buf): 93 | assert self.h 94 | assert buf 95 | LOG.debug('Calling nfq_handle_packet(%s, buf, %s)', self.h, len(buf)) 96 | self.dll.nfq_handle_packet(self.h, buf, len(buf)) 97 | LOG.debug('Called nfq_handle_packet') 98 | 99 | @classmethod 100 | def cb_get_payload(cls, nfad): 101 | """ 102 | int nfq_get_payload(struct nfq_data * nfad, unsigned char ** data) 103 | """ 104 | assert nfad 105 | payload = POINTER(c_char)() 106 | LOG.debug('Calling nfq_get_payload') 107 | len = cls.dll.nfq_get_payload(nfad, byref(payload)) 108 | LOG.debug('Called nfq_get_payload') 109 | ret_str = string_at(payload, len) 110 | return ret_str 111 | 112 | @classmethod 113 | def cb_get_packet_id(cls, nfad): 114 | assert nfad 115 | LOG.debug('Calling nfq_get_msg_packet_hdr') 116 | phdr = cls.dll.nfq_get_msg_packet_hdr(nfad) 117 | LOG.debug('Called nfq_get_msg_packet_hdr') 118 | assert phdr 119 | packet_id = ntohl(phdr.contents.packet_id) 120 | return packet_id 121 | 122 | @classmethod 123 | def cb_set_verdict(cls, qh, packet_id, verdict): 124 | """ 125 | int nfq_set_verdict (struct nfq_q_handle * qh, 126 | u_int32_t id, 127 | u_int32_t verdict, 128 | u_int32_t data_len, 129 | const unsigned char * buf) 130 | """ 131 | LOG.debug('Calling nfq_set_verdict') 132 | ret = cls.dll.nfq_set_verdict(qh, packet_id, verdict, 0, 0) 133 | LOG.debug('Called nfq_set_verdict') 134 | return ret 135 | 136 | 137 | if __name__ == '__main__': 138 | # example usage of hookswitch.internal.nfq 139 | # TODO: move to unittests 140 | Q_NUM = 42 141 | SOCK_BUF_SIZE = 65536 142 | import socket 143 | from hexdump import hexdump 144 | 145 | def cb(qh, nfmsg, nfad, data): 146 | """ 147 | int nfq_callback(struct nfq_q_handle *qh, 148 | struct nfgenmsg *nfmsg, 149 | struct nfq_data *nfad, void *data); 150 | """ 151 | print('===CB===') 152 | LOG.info('CB called with data=%s', data) 153 | payload = NFQ.cb_get_payload(nfad) 154 | packet_id = NFQ.cb_get_packet_id(nfad) 155 | LOG.info('ID %d: %s', packet_id) 156 | hexdump(payload) 157 | NFQ.cb_set_verdict(qh, packet_id, NFQ.NF_ACCEPT) 158 | return 1 159 | 160 | # https://github.com/JohannesBuchner/PyMultiNest/issues/5 161 | cb_c = NFQ.CALLBACK_CFUNCTYPE(cb) 162 | nfq = NFQ(Q_NUM, cb_c) 163 | s = socket.fromfd(nfq.fd, socket.AF_UNIX, socket.SOCK_STREAM) 164 | while True: 165 | d = s.recv(SOCK_BUF_SIZE) 166 | assert d 167 | nfq.handle_packet(d) 168 | s.close() 169 | nfq.close() 170 | -------------------------------------------------------------------------------- /hookswitch/internal/zmqclient.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | import eventlet 3 | from eventlet.green import zmq 4 | import six 5 | import json 6 | 7 | from hookswitch.internal import LOG as _LOG 8 | 9 | LOG = _LOG.getChild(__name__) 10 | 11 | 12 | @six.add_metaclass(ABCMeta) 13 | class ZMQClientBase(object): 14 | 15 | def __init__(self, zmq_addr): 16 | self.zmq_addr = zmq_addr 17 | self.zmq_ctx = zmq.Context() 18 | self.zs = self.zmq_ctx.socket(zmq.PAIR) 19 | self.pendings = {} # key: id(int), value: Ethernet Frame 20 | 21 | def start(self): 22 | LOG.debug('Connecting to ZMQ: %s', self.zmq_addr) 23 | self.zs.connect(self.zmq_addr) 24 | worker_handle = eventlet.spawn(self._zmq_worker) 25 | return worker_handle 26 | 27 | def _zmq_worker(self): 28 | while True: 29 | # eth_ignored will be used when 'op' == 'modify' is implemented 30 | metadata_str, eth_ignored = self.zs.recv_multipart() 31 | metadata = json.loads(metadata_str) 32 | op = metadata['op'] 33 | assert op in ('accept', 'drop'), \ 34 | 'Invalid op packet metadata: %s (pendings=%s)' % ( 35 | metadata, self.pendings) 36 | packet_id = metadata['id'] 37 | assert packet_id in self.pendings, \ 38 | 'Unknown packet metadata: %s (pendings=%s)' % ( 39 | metadata, self.pendings) 40 | eth_bytes = self.pendings[packet_id] 41 | del self.pendings[packet_id] 42 | LOG.debug('Pendings-(id=%d,op=%s): %d->%d', packet_id, 43 | op, len(self.pendings) + 1, len(self.pendings)) 44 | if op == 'accept': 45 | self.on_accept(packet_id, eth_bytes, metadata) 46 | elif op == 'drop': 47 | self.on_drop(packet_id, eth_bytes, metadata) 48 | else: 49 | raise RuntimeError('This should not happen') 50 | 51 | def send(self, packet_id, eth_bytes): 52 | metadata = {'id': packet_id} 53 | metadata_str = json.dumps(metadata) 54 | self.zs.send_multipart((metadata_str, eth_bytes)) 55 | assert packet_id not in self.pendings, \ 56 | 'Bad packet metadata: %s (pendings=%s)' % (metadata, self.pendings) 57 | self.pendings[packet_id] = eth_bytes 58 | LOG.debug('Pendings+(id=%d): %d->%d', packet_id, 59 | len(self.pendings) - 1, len(self.pendings)) 60 | 61 | @abstractmethod 62 | def on_accept(self, packet_id, eth_bytes, metadata=None): 63 | pass 64 | 65 | @abstractmethod 66 | def on_drop(self, packet_id, eth_bytes, metadata=None): 67 | pass 68 | -------------------------------------------------------------------------------- /hookswitch/nfq.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import six 3 | import sys 4 | import eventlet 5 | from eventlet.green import socket 6 | from hookswitch.internal import LOG as _LOG 7 | from hookswitch.internal.nfq import NFQ 8 | from hookswitch.internal.zmqclient import ZMQClientBase 9 | 10 | LOG = _LOG.getChild(__name__) 11 | 12 | 13 | def nfq_hook_cb(qh, nfmsg, nfad, data): 14 | """ 15 | int nfq_callback(struct nfq_q_handle *qh, 16 | struct nfgenmsg *nfmsg, 17 | struct nfq_data *nfad, void *data); 18 | """ 19 | packet_id = NFQ.cb_get_packet_id(nfad) 20 | ip_bytes = NFQ.cb_get_payload(nfad) 21 | this_id = data 22 | this = ctypes.cast(this_id, ctypes.py_object).value 23 | assert isinstance(this, NFQHook) 24 | this.send_ip_packet_to_controller(packet_id, ip_bytes) 25 | return 1 26 | 27 | 28 | # decl position matters: 29 | # https://github.com/JohannesBuchner/PyMultiNest/issues/5 30 | nfq_hook_cb_c = NFQ.CALLBACK_CFUNCTYPE(nfq_hook_cb) 31 | 32 | 33 | class NFQHookZMQC(ZMQClientBase): 34 | 35 | def __init__(self, zmq_addr, nfq): 36 | super(NFQHookZMQC, self).__init__(zmq_addr) 37 | self.nfq = nfq 38 | 39 | def on_accept(self, packet_id, eth_bytes, metadata=None): 40 | NFQ.cb_set_verdict(self.nfq.qh, packet_id, NFQ.NF_ACCEPT) 41 | 42 | def on_drop(self, packet_id, eth_bytes, metadata=None): 43 | NFQ.cb_set_verdict(self.nfq.qh, packet_id, NFQ.NF_DROP) 44 | 45 | 46 | class NFQHook(object): 47 | NFQ_SOCKET_BUFFER_SIZE = 1024 * 4 48 | 49 | def __init__(self, nfq_number, zmq_addr): 50 | LOG.debug('NFQ Number: %s', nfq_number) 51 | assert isinstance(nfq_number, int) 52 | self.self_id = id(self) 53 | self.nfq = NFQ(nfq_number, 54 | nfq_hook_cb_c, 55 | ctypes.c_void_p(self.self_id)) 56 | self.zmq_client = NFQHookZMQC(zmq_addr, self.nfq) 57 | 58 | def start(self): 59 | zmq_worker_handle = self.zmq_client.start() 60 | nfq_worker_handle = eventlet.spawn(self._nfq_worker) 61 | nfq_worker_handle.wait() 62 | zmq_worker_handle.wait() 63 | raise RuntimeError('should not reach here') 64 | 65 | def _nfq_worker(self): 66 | s = socket.fromfd(self.nfq.fd, socket.AF_UNIX, socket.SOCK_STREAM) 67 | try: 68 | while True: 69 | LOG.debug('NFQ Recv') 70 | nfad = s.recv(self.NFQ_SOCKET_BUFFER_SIZE) 71 | self.nfq.handle_packet(nfad) 72 | LOG.error('NFQ Worker loop leave!') 73 | finally: 74 | LOG.error('NFQ Worker closing socket!') 75 | s.close() 76 | self.nfq.close() 77 | sys.exit(1) 78 | 79 | def send_ip_packet_to_controller(self, packet_id, ip_bytes): 80 | LOG.debug('Sending packet %d to controller', packet_id) 81 | 82 | # from hexdump import hexdump 83 | # for l in hexdump(ip_bytes, result='generator'): 84 | # LOG.debug(l) 85 | 86 | # TODO: eliminate dummy eth header 87 | dummy_eth = b'\xff\xff\xff\xff\xff\xff' + \ 88 | '\x00\x00\x00\x00\x00\x00' + '\x08\x00' + six.b(ip_bytes) 89 | self.zmq_client.send(packet_id, dummy_eth) 90 | -------------------------------------------------------------------------------- /hookswitch/openflow.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | import six 3 | import socket 4 | from ryu.controller import ofp_event 5 | from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER 6 | from ryu.controller.handler import set_ev_cls 7 | from ryu.ofproto import ofproto_v1_3, ofproto_v1_3_parser 8 | from ryu.lib.packet import packet 9 | from ryu.lib.packet import ethernet 10 | from ryu.app.simple_switch_13 import SimpleSwitch13 11 | from hookswitch.internal.zmqclient import ZMQClientBase 12 | 13 | 14 | class OFHookZMQC(ZMQClientBase): 15 | 16 | def __init__(self, zmq_addr, ryu, datapath): 17 | super(OFHookZMQC, self).__init__(zmq_addr) 18 | self.ryu = ryu 19 | self.datapath = datapath 20 | 21 | def on_accept(self, packet_id_IGNORED, eth_bytes, metadata=None): 22 | ofp = self.datapath.ofproto 23 | parser = self.datapath.ofproto_parser 24 | in_port, out_port = self.ryu.determine_ports(self.datapath, eth_bytes) 25 | ofp_packet_out = parser.OFPPacketOut(datapath=self.datapath, 26 | buffer_id=ofp.OFP_NO_BUFFER, 27 | in_port=in_port, 28 | actions=[ 29 | parser.OFPActionOutput( 30 | out_port)], 31 | data=eth_bytes) 32 | self.datapath.send_msg(ofp_packet_out) 33 | 34 | def on_drop(self, packet_id, eth_bytes, metadata=None): 35 | pass 36 | 37 | 38 | @six.add_metaclass(ABCMeta) 39 | class OF13HookBase(SimpleSwitch13): 40 | 41 | """ 42 | Inherits ryu.app.SimpleSwitch13. 43 | Tested with ryu 3.20.2 + OVS 2.3.1. 44 | """ 45 | OFP = ofproto_v1_3 46 | OFP_PARSER = ofproto_v1_3_parser # used by child classes 47 | FLOW_PRIORITY_BASE = 10240 48 | 49 | def __init__(self, matches, zmq_addr, *args, **kwargs): 50 | super(OF13HookBase, self).__init__(*args, **kwargs) 51 | self.matches = matches 52 | self.zmq_client = None 53 | self.zmq_addr = zmq_addr 54 | self.current_packet_id = 0 55 | 56 | def determine_ports(self, datapath, data): 57 | ofp = datapath.ofproto 58 | in_port = ofp.OFPP_CONTROLLER 59 | out_port = ofp.OFPP_FLOOD 60 | 61 | pkt = packet.Packet(data) 62 | eth = pkt.get_protocols(ethernet.ethernet)[0] 63 | # self.mac_to_port is managed by the parent class 64 | if eth.src in self.mac_to_port[datapath.id]: 65 | in_port = self.mac_to_port[datapath.id][eth.src] 66 | if eth.dst in self.mac_to_port[datapath.id]: 67 | out_port = self.mac_to_port[datapath.id][eth.dst] 68 | return (in_port, out_port) 69 | 70 | @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) 71 | def switch_features_handler(self, ev): 72 | super(OF13HookBase, self).switch_features_handler(ev) 73 | datapath = ev.msg.datapath 74 | ofp = datapath.ofproto 75 | parser = datapath.ofproto_parser 76 | 77 | # increase miss_send_len to the max (OVS default: 128 bytes) 78 | conf = parser.OFPSetConfig(datapath, ofp.OFPC_FRAG_NORMAL, 0xFFFF) 79 | datapath.send_msg(conf) 80 | 81 | # install self.matches 82 | actions = [parser.OFPActionOutput(ofp.OFPP_CONTROLLER, 83 | ofp.OFPCML_NO_BUFFER)] 84 | inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, 85 | actions)] 86 | for i, match in enumerate(self.matches): 87 | mod = parser.OFPFlowMod(datapath=datapath, 88 | priority=self.FLOW_PRIORITY_BASE + i, 89 | match=match, 90 | instructions=inst) 91 | # TODO: Check flow overlaps 92 | datapath.send_msg(mod) 93 | 94 | # initialize mac table (managed by the parent class) 95 | self.mac_to_port.setdefault(datapath.id, {}) 96 | 97 | # Start ZMQ Worker 98 | assert self.zmq_client is None, 'multiple datapaths not supported' 99 | self.zmq_client = OFHookZMQC(self.zmq_addr, self, datapath) 100 | self.zmq_client_handle = self.zmq_client.start() 101 | 102 | self.logger.debug('Setup done for datapath %d', datapath.id) 103 | 104 | @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) 105 | def _packet_in_handler(self, ev): 106 | msg = ev.msg 107 | # NOTE: too old OVS(<= 2.0?) returns OFPR_ACTION, not OFPR_NO_MATCH 108 | # http://git.openvswitch.org/cgi-bin/gitweb.cgi?p=openvswitch;a=commitdiff;h=cfa955b083c5617212a29a03423e063ff6cb350a 109 | # So we need OVS >= 2.1. 110 | if msg.reason == ev.msg.datapath.ofproto.OFPR_NO_MATCH: 111 | super(OF13HookBase, self)._packet_in_handler(ev) 112 | else: 113 | assert self.zmq_client, \ 114 | 'PKT-IN handler called before configuration' 115 | # NOTE: packet_id can conflict when packet_id = hash(ev) 116 | self.zmq_client.send(self.current_packet_id, msg.data) 117 | self.current_packet_id += 1 118 | 119 | 120 | class OF13Hook(OF13HookBase): 121 | 122 | def __init__(self, tcp_ports, udp_ports, zmq_addr, *args, **kwargs): 123 | OFPMatch = self.OFP_PARSER.OFPMatch 124 | matches = [] 125 | for port in tcp_ports: 126 | m = OFPMatch( 127 | eth_type=0x0800, ip_proto=socket.IPPROTO_TCP, tcp_dst=port) 128 | matches.append(m) 129 | m = OFPMatch( 130 | eth_type=0x0800, ip_proto=socket.IPPROTO_TCP, tcp_src=port) 131 | matches.append(m) 132 | for port in udp_ports: 133 | m = OFPMatch( 134 | eth_type=0x0800, ip_proto=socket.IPPROTO_UDP, udp_dst=port) 135 | matches.append(m) 136 | m = OFPMatch( 137 | eth_type=0x0800, ip_proto=socket.IPPROTO_UDP, udp_src=port) 138 | matches.append(m) 139 | super(OF13Hook, self).__init__(matches, zmq_addr, *args, **kwargs) 140 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | setup( 3 | name='hookswitch', 4 | packages=['hookswitch', 'hookswitch.cli', 'hookswitch.internal'], 5 | version='0.0.2', 6 | description='A usermode packet injection library', 7 | author='Akihiro Suda', 8 | author_email='suda.akihiro@lab.ntt.co.jp', 9 | url='https://github.com/osrg/hookswitch', 10 | download_url='https://github.com/osrg/hookswitch/tarball/v0.0.2', 11 | license='Apache License 2.0', 12 | scripts=[ 13 | 'bin/hookswitch-nfq', 14 | 'bin/hookswitch-of13', 15 | 'bin/hookswitch-example-controller', 16 | ], 17 | install_requires=[ 18 | 'hexdump', 19 | 'python-prctl', # FIXME: prctl is only available for Linux 20 | 'pyzmq', 21 | 'ryu', # ryu automatically installs: abc, eventlet, six, .. 22 | ], 23 | keywords=['fault', 'injection', 'testing', 24 | 'openvswitch', 'netfilter', 'ryu'], 25 | classifiers=[ 26 | 'License :: OSI Approved :: Apache Software License', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2', 29 | # 'Programming Language :: Python :: 3', 30 | 'Topic :: Software Development :: Libraries', 31 | 'Topic :: System :: Networking' 32 | ], 33 | ) 34 | --------------------------------------------------------------------------------