├── docs ├── .nojekyll ├── objects.inv ├── _static │ ├── file.png │ ├── plus.png │ ├── minus.png │ ├── contents.png │ ├── navigation.png │ ├── documentation_options.js │ ├── pygments.css │ └── doctools.js ├── _images │ ├── packet.png │ ├── applayer.png │ ├── srpyarch.png │ └── applayer_detail.png ├── .buildinfo ├── _sources │ ├── thanks.rst.txt │ ├── index.rst.txt │ ├── release_notes.rst.txt │ ├── installation.rst.txt │ └── intro.rst.txt ├── search.html ├── py-modindex.html ├── _modules │ └── index.html └── thanks.html ├── switchyard ├── __init__.py ├── lib │ ├── __init__.py │ ├── openflow │ │ └── __init__.py │ ├── .gitignore │ ├── packet │ │ ├── .gitignore │ │ ├── __init__.py │ │ ├── util.py │ │ ├── null.py │ │ ├── udp.py │ │ └── arp.py │ ├── socket │ │ └── __init__.py │ ├── topo │ │ ├── __init__.py │ │ └── util.py │ ├── userlib.py │ ├── debugging.py │ ├── exceptions.py │ ├── logging.py │ └── interface.py ├── sim │ ├── __init__.py │ ├── linkem.py │ └── nodeexec.py ├── outputfmt.py ├── importcode.py ├── textcolor.py └── swyard.py ├── documentation ├── .gitignore ├── packet.pdf ├── packet.png ├── applayer.pdf ├── applayer.png ├── srpyarch.pdf ├── srpyarch.png ├── packet.graffle ├── applayer_detail.pdf ├── applayer_detail.png ├── code │ ├── emptytestscenario.py │ ├── failhub4.py │ ├── run_example.py │ ├── failhub1.py │ ├── failhub2.py │ ├── inout1.py │ ├── failhub3.py │ ├── failhub8.py │ ├── failhub7.py │ ├── failhub6.py │ ├── badscenario1.py │ ├── clientsocketapp.py │ ├── inoutloop.py │ ├── inout2.py │ ├── enterdebugger.py │ ├── testscenario1.py │ ├── fullhub.py │ ├── failhub5.py │ ├── intfex.py │ ├── fullhub2.py │ ├── baaadhub.py │ ├── inouttest.py │ ├── protostackpattern.py │ ├── testscenario2.py │ ├── pkt_construction.py │ ├── newheader.py │ ├── udpappheader.py │ └── hubtests.py ├── README.md ├── thanks.rst ├── index.rst ├── release_notes.rst ├── installation.rst └── intro.rst ├── requirements.txt ├── examples ├── exercises │ ├── applayer │ │ ├── msgboarddiag.pdf │ │ ├── msgboarddiag.png │ │ ├── msgboarddiag.graffle │ │ ├── msgboardmiddlebox.rst │ │ └── applayer.rst │ ├── router │ │ ├── router1_pcap1.png │ │ ├── router1_pcap2.png │ │ ├── router2_pcap.png │ │ ├── dvroute │ │ │ ├── rippkt1.png │ │ │ ├── dv_topology.pdf │ │ │ ├── dv_topology.png │ │ │ └── dv_topology.graffle │ │ │ │ ├── data.plist │ │ │ │ └── image1.pdf │ │ ├── router2_topology.png │ │ ├── router_topology.png │ │ ├── lsroute │ │ │ ├── ls_topology.pdf │ │ │ ├── ls_topology.png │ │ │ └── ls_topology.graffle │ │ │ │ ├── data.plist │ │ │ │ └── image1.pdf │ │ ├── router2_topology.graffle │ │ │ ├── data.plist │ │ │ └── image1.pdf │ │ ├── router_topology.graffle │ │ │ ├── data.plist │ │ │ └── image1.pdf │ │ ├── forwarding_table.txt │ │ ├── router4.rst │ │ ├── myrouter.py │ │ └── start_mininet.py │ ├── firewall │ │ ├── firewall_topology.png │ │ ├── firewall_topology.graffle │ │ │ ├── data.plist │ │ │ ├── image1.pdf │ │ │ └── image2.pdf │ │ ├── www │ │ │ └── start_webserver.sh │ │ ├── firewall.py │ │ ├── firewall_rules.txt │ │ ├── start_mininet.py │ │ └── impairmenttest.py │ ├── learning_switch │ │ ├── ls_diagram.png │ │ ├── ls_flowchart.png │ │ ├── ls_flowchart.graffle │ │ ├── ls_diagram.graffle │ │ │ ├── data.plist │ │ │ └── image1.pdf │ │ ├── myswitch.py │ │ └── switchtopo.py │ └── README.rst ├── server_udpstackex.py ├── clientapp_udpstackex.py ├── sniff.py ├── readpkt.py ├── sydump.py ├── udpstack_tests.py ├── sendpkt.py ├── myhub.py ├── udpstack.py ├── hubtests.py └── README.rst ├── .gitignore ├── tests ├── test_rwimg.py ├── test_addr.py ├── test_null.py ├── test_udp.py ├── test_color.py ├── test_tcp.py ├── test_ethernet.py ├── test_importer.py ├── test_arp.py ├── test_ofswitch_ext.py ├── test_ripv2.py ├── test_pcapffi.py └── test_hostfirewall.py ├── Dockerfile ├── runtests.sh ├── .travis.yml ├── setup.py └── README.rst /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /switchyard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /switchyard/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /switchyard/sim/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /switchyard/lib/openflow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /switchyard/lib/.gitignore: -------------------------------------------------------------------------------- 1 | testpcap.py 2 | -------------------------------------------------------------------------------- /documentation/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | x.txt 3 | -------------------------------------------------------------------------------- /switchyard/lib/packet/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /switchyard/lib/socket/__init__.py: -------------------------------------------------------------------------------- 1 | from .socketemu import * 2 | -------------------------------------------------------------------------------- /docs/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/objects.inv -------------------------------------------------------------------------------- /switchyard/lib/topo/__init__.py: -------------------------------------------------------------------------------- 1 | from .util import * 2 | from .topobuild import * 3 | -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_static/plus.png -------------------------------------------------------------------------------- /docs/_images/packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_images/packet.png -------------------------------------------------------------------------------- /docs/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_static/minus.png -------------------------------------------------------------------------------- /documentation/packet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/packet.pdf -------------------------------------------------------------------------------- /documentation/packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/packet.png -------------------------------------------------------------------------------- /docs/_images/applayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_images/applayer.png -------------------------------------------------------------------------------- /docs/_images/srpyarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_images/srpyarch.png -------------------------------------------------------------------------------- /docs/_static/contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_static/contents.png -------------------------------------------------------------------------------- /documentation/applayer.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/applayer.pdf -------------------------------------------------------------------------------- /documentation/applayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/applayer.png -------------------------------------------------------------------------------- /documentation/srpyarch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/srpyarch.pdf -------------------------------------------------------------------------------- /documentation/srpyarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/srpyarch.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi>=1.14.0 2 | colorama>=0.4.3 3 | coverage>=5.1 4 | networkx>=2.4 5 | psutil>=5.7.0 6 | -------------------------------------------------------------------------------- /docs/_static/navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_static/navigation.png -------------------------------------------------------------------------------- /documentation/packet.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/packet.graffle -------------------------------------------------------------------------------- /docs/_images/applayer_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/docs/_images/applayer_detail.png -------------------------------------------------------------------------------- /documentation/applayer_detail.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/applayer_detail.pdf -------------------------------------------------------------------------------- /documentation/applayer_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/documentation/applayer_detail.png -------------------------------------------------------------------------------- /documentation/code/emptytestscenario.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | scenario = TestScenario("test example") 4 | -------------------------------------------------------------------------------- /examples/exercises/applayer/msgboarddiag.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/applayer/msgboarddiag.pdf -------------------------------------------------------------------------------- /examples/exercises/applayer/msgboarddiag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/applayer/msgboarddiag.png -------------------------------------------------------------------------------- /examples/exercises/router/router1_pcap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router1_pcap1.png -------------------------------------------------------------------------------- /examples/exercises/router/router1_pcap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router1_pcap2.png -------------------------------------------------------------------------------- /examples/exercises/router/router2_pcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router2_pcap.png -------------------------------------------------------------------------------- /examples/exercises/router/dvroute/rippkt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/dvroute/rippkt1.png -------------------------------------------------------------------------------- /examples/exercises/router/router2_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router2_topology.png -------------------------------------------------------------------------------- /examples/exercises/router/router_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router_topology.png -------------------------------------------------------------------------------- /examples/exercises/applayer/msgboarddiag.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/applayer/msgboarddiag.graffle -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/firewall/firewall_topology.png -------------------------------------------------------------------------------- /examples/exercises/learning_switch/ls_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/learning_switch/ls_diagram.png -------------------------------------------------------------------------------- /examples/exercises/router/dvroute/dv_topology.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/dvroute/dv_topology.pdf -------------------------------------------------------------------------------- /examples/exercises/router/dvroute/dv_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/dvroute/dv_topology.png -------------------------------------------------------------------------------- /examples/exercises/router/lsroute/ls_topology.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/lsroute/ls_topology.pdf -------------------------------------------------------------------------------- /examples/exercises/router/lsroute/ls_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/lsroute/ls_topology.png -------------------------------------------------------------------------------- /examples/exercises/learning_switch/ls_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/learning_switch/ls_flowchart.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xenv 2 | *.pyc 3 | __pycache__ 4 | *.pcap 5 | .coverage 6 | htmlcov 7 | sandbox 8 | updatepypi.sh 9 | switchyard.egg-info 10 | build 11 | dist 12 | -------------------------------------------------------------------------------- /examples/exercises/learning_switch/ls_flowchart.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/learning_switch/ls_flowchart.graffle -------------------------------------------------------------------------------- /examples/exercises/router/router2_topology.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router2_topology.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/router/router2_topology.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router2_topology.graffle/image1.pdf -------------------------------------------------------------------------------- /examples/exercises/router/router_topology.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router_topology.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/router/router_topology.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/router_topology.graffle/image1.pdf -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall_topology.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/firewall/firewall_topology.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall_topology.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/firewall/firewall_topology.graffle/image1.pdf -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall_topology.graffle/image2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/firewall/firewall_topology.graffle/image2.pdf -------------------------------------------------------------------------------- /examples/exercises/learning_switch/ls_diagram.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/learning_switch/ls_diagram.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/learning_switch/ls_diagram.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/learning_switch/ls_diagram.graffle/image1.pdf -------------------------------------------------------------------------------- /examples/exercises/router/dvroute/dv_topology.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/dvroute/dv_topology.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/router/dvroute/dv_topology.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/dvroute/dv_topology.graffle/image1.pdf -------------------------------------------------------------------------------- /examples/exercises/router/lsroute/ls_topology.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/lsroute/ls_topology.graffle/data.plist -------------------------------------------------------------------------------- /examples/exercises/router/lsroute/ls_topology.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsommers/switchyard/HEAD/examples/exercises/router/lsroute/ls_topology.graffle/image1.pdf -------------------------------------------------------------------------------- /documentation/code/failhub4.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | net.clone_packet() 7 | -------------------------------------------------------------------------------- /documentation/code/run_example.py: -------------------------------------------------------------------------------- 1 | from code import InteractiveConsole 2 | import sys 3 | 4 | console = InteractiveConsole() 5 | for line in sys.stdin: 6 | print(">>> {}".format(line.strip())) 7 | console.push(line.strip()) 8 | 9 | -------------------------------------------------------------------------------- /examples/exercises/router/forwarding_table.txt: -------------------------------------------------------------------------------- 1 | 172.16.0.0 255.255.0.0 192.168.1.2 router-eth0 2 | 172.16.128.0 255.255.192.0 10.10.0.254 router-eth1 3 | 172.16.64.0 255.255.192.0 10.10.1.254 router-eth1 4 | 10.100.0.0 255.255.0.0 172.16.42.2 router-eth2 -------------------------------------------------------------------------------- /documentation/code/failhub1.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | net.send_packet('eth0', packet) 7 | -------------------------------------------------------------------------------- /documentation/code/failhub2.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | # net.send_packet('eth0', packet) 7 | -------------------------------------------------------------------------------- /documentation/code/inout1.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | net.send_packet(input_port, packet) 7 | -------------------------------------------------------------------------------- /docs/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 30eb21c01711bfe8ce05dc0c79cca030 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /documentation/code/failhub3.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | timestamp,input_port,packet = net.recv_packet() 7 | -------------------------------------------------------------------------------- /documentation/code/failhub8.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | del packet[-1] 7 | net.send_packet("eth0", packet) 8 | -------------------------------------------------------------------------------- /documentation/code/failhub7.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | packet[-1] = TCP() 7 | net.send_packet("eth0", packet) 8 | -------------------------------------------------------------------------------- /documentation/code/failhub6.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | timestamp,input_port,packet = net.recv_packet() 5 | print ("Received {} on {}".format(packet, input_port)) 6 | packet[Ethernet].src = "ba:aa:aa:ba:aa:aa" 7 | net.send_packet("eth0", packet) 8 | -------------------------------------------------------------------------------- /documentation/code/badscenario1.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | scenario = TestScenario("test example") 4 | scenario.add_interface('eth0', 'ab:cd:ef:ab:cd:ef', '1.2.3.4', '255.255.0.0', iftype=InterfaceType.Wired) 5 | 6 | scenario.expect(PacketInputEvent('eth1', Packet()), "A packet knocks on the door...") 7 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Switchyard documentation 2 | 3 | This directory contains Sphinx source (http://sphinx-doc.org/) for Switchyard documentation. 4 | 5 | ## License 6 | 7 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 8 | http://creativecommons.org/licenses/by-nc-sa/4.0/ 9 | -------------------------------------------------------------------------------- /switchyard/outputfmt.py: -------------------------------------------------------------------------------- 1 | class VerboseOutput(object): 2 | _on = False 3 | 4 | @staticmethod 5 | def enable(): 6 | VerboseOutput._on = True 7 | 8 | @staticmethod 9 | def disable(): 10 | VerboseOutput._on = False 11 | 12 | @staticmethod 13 | def enabled(): 14 | return VerboseOutput._on 15 | -------------------------------------------------------------------------------- /switchyard/lib/packet/__init__.py: -------------------------------------------------------------------------------- 1 | from .packet import * 2 | from .common import * 3 | 4 | from .ethernet import * 5 | from .arp import * 6 | 7 | from .ipv4 import * 8 | from .ipv6 import * 9 | from .icmpv6 import * 10 | 11 | from .udp import * 12 | from .tcp import * 13 | from .icmp import * 14 | 15 | from .ripv2 import * 16 | 17 | from .util import * 18 | 19 | from .null import * 20 | 21 | -------------------------------------------------------------------------------- /docs/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | const DOCUMENTATION_OPTIONS = { 2 | VERSION: '1.0.1', 3 | LANGUAGE: 'en', 4 | COLLAPSE_INDEX: false, 5 | BUILDER: 'html', 6 | FILE_SUFFIX: '.html', 7 | LINK_SUFFIX: '.html', 8 | HAS_SOURCE: true, 9 | SOURCELINK_SUFFIX: '.txt', 10 | NAVIGATION_WITH_KEYS: false, 11 | SHOW_SEARCH_SUMMARY: true, 12 | ENABLE_SEARCH_SHORTCUTS: true, 13 | }; -------------------------------------------------------------------------------- /examples/server_udpstackex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import socket 3 | HOST = '127.0.0.1' 4 | PORT = 10000 5 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 6 | s.bind((HOST, PORT)) 7 | print("Server waiting on port {} for a message to echo back".format(PORT)) 8 | data,addr = s.recvfrom(1024) 9 | print("Received {} from {}".format(repr(data), repr(addr))) 10 | x = s.sendto(data, (addr[0],addr[1])) 11 | s.close() 12 | 13 | -------------------------------------------------------------------------------- /tests/test_rwimg.py: -------------------------------------------------------------------------------- 1 | import os 2 | from switchyard.lib.topo import * 3 | 4 | t = Topology() 5 | h1 = t.addHost() 6 | h2 = t.addHost() 7 | s1 = t.addSwitch() 8 | s2 = t.addSwitch() 9 | t.addLink(h1,s1,1000000,0.1) 10 | t.addLink(h2,s2,1000000,0.1) 11 | t.addLink(s1,s2,1000000,"1 microsec") 12 | 13 | save_to_file(t, 'xtopo.txt') 14 | save_graph(t, 'xtopo.png', showaddrs=False, showintfs=True) 15 | 16 | os.unlink('xtopo.txt') 17 | os.unlink('xtopo.png') 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get -y update && DEBIAN_FRONTEND=noninteractive TZ="America/New_York" apt-get -y install build-essential git-core vim make gcc clang libcurl3-gnutls-dev curl wget libxml2-dev libcurl4-gnutls-dev libssh2-1-dev libz-dev libssl-dev libreadline-dev automake libtool bison manpages-dev manpages-posix-dev net-tools man-db libffi-dev libpcap-dev python3-dev python3-pip python3-venv mininet 3 | RUN pip3 install switchyard 4 | WORKDIR swyard 5 | COPY ./examples examples 6 | CMD bash 7 | -------------------------------------------------------------------------------- /examples/exercises/firewall/www/start_webserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # assume we're running this from parent directory (within mininet) 4 | cd www 5 | 6 | # create a couple files for testing 7 | echo ' Test file 1 sneaky crackers wuz here! ' > 1.html 8 | dd if=/dev/zero of=bigfile bs=1k count=100 2>&1 > /dev/null 9 | 10 | if (( $# == 0 )) ; then 11 | port=80 12 | else 13 | port=$1 14 | fi 15 | 16 | python3 -m http.server ${port} & 17 | 18 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | export PYTHONPATH=`pwd`/../..:`pwd` 4 | coverage erase 5 | rm -rf htmlcov 6 | 7 | PAT='switchyard/*','switchyard/switch*' 8 | EXCLPAT='*__init__.py','switchyard/sim/*','switchyard/lib/openflow/*' 9 | 10 | for f in tests/*.py 11 | do 12 | # python3 $f 13 | # coverage run --source '.,switchyard' --omit '*xenv*' --include ${PAT} -a $f 14 | coverage run --source '.,switchyard' --omit '*xenv*' -a $f 15 | done 16 | 17 | coverage html --include ${PAT} --omit ${EXCLPAT} 18 | coverage report --include ${PAT} --omit ${EXCLPAT} 19 | 20 | -------------------------------------------------------------------------------- /examples/clientapp_udpstackex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # import socket 4 | import switchyard.lib.socket as socket 5 | 6 | HOST = '127.0.0.1' 7 | PORT = 10000 8 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | s.settimeout(2.0) 10 | 11 | print("Sending message to server at {},{}".format(HOST,PORT)) 12 | s.sendto(b'Hello, stack', (HOST,PORT)) 13 | try: 14 | data,addr = s.recvfrom(1024) 15 | print('Client socket application received message from {}: {}'.format(repr(addr),data.decode('utf8'))) 16 | except: 17 | print("Timeout") 18 | 19 | s.close() 20 | -------------------------------------------------------------------------------- /documentation/code/clientsocketapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # import socket 4 | import switchyard.lib.socket as socket 5 | 6 | HOST = '127.0.0.1' 7 | PORT = 10000 8 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | s.settimeout(2.0) 10 | 11 | print("Sending message to server at {},{}".format(HOST,PORT)) 12 | s.sendto(b'Hello, stack', (HOST,PORT)) 13 | try: 14 | data,addr = s.recvfrom(1024) 15 | print('Client socket application received message from {}: {}'.format(repr(addr),data.decode('utf8'))) 16 | except: 17 | print("Timeout") 18 | 19 | s.close() 20 | -------------------------------------------------------------------------------- /documentation/code/inoutloop.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | while True: 5 | try: 6 | timestamp,input_port,packet = net.recv_packet() 7 | except Shutdown: 8 | log_info ("Got shutdown signal; exiting") 9 | break 10 | except NoPackets: 11 | log_info ("No packets were available.") 12 | continue 13 | 14 | # if we get here, we must have received a packet 15 | log_info ("Received {} on {}".format(packet, input_port)) 16 | net.send_packet(input_port, packet) 17 | -------------------------------------------------------------------------------- /switchyard/lib/userlib.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a wrapper module to facilitate easy import of the various modules, functions, classes, and other items needed from the perspective of a user program in Switchyard. 3 | ''' 4 | from .packet import * 5 | from .address import * 6 | from .exceptions import * 7 | from .logging import log_debug, log_info, log_failure, log_warn 8 | from .interface import Interface, InterfaceType 9 | from .testing import PacketInputEvent, PacketOutputEvent, PacketInputTimeoutEvent, TestScenario 10 | from .debugging import debugger 11 | from .socket.socketemu import ApplicationLayer 12 | -------------------------------------------------------------------------------- /documentation/code/inout2.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | # below, recvdata is a namedtuple 5 | recvdata = net.recv_packet() 6 | print ("At {}, received {} on {}".format( 7 | recvdata.timestamp, recvdata.packet, recvdata.input_port)) 8 | 9 | # alternatively, the above line could use indexing, although 10 | # readability suffers: 11 | # recvdata[0], recvdata[2], recvdata[1])) 12 | 13 | net.send_packet(recvdata.input_port, recvdata.packet) 14 | 15 | # likewise, the above line could be written using indexing 16 | # but, again, readability suffers: 17 | # net.send_packet(recvdata[1], recvdata[2]) 18 | -------------------------------------------------------------------------------- /examples/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Packet sniffer in Python 5 | ''' 6 | 7 | from switchyard.lib.userlib import * 8 | 9 | def main(net): 10 | my_interfaces = net.interfaces() 11 | log_info ("My interfaces: {}".format([intf.name for intf in my_interfaces])) 12 | count = 0 13 | while True: 14 | try: 15 | timestamp,dev,packet = net.recv_packet(timeout=1.0) 16 | except NoPackets: 17 | continue 18 | except Shutdown: 19 | return 20 | 21 | log_info("{:.3f}: {} {}".format(timestamp,dev,packet)) 22 | count += 1 23 | 24 | net.shutdown() 25 | print ("Got {} packets.".format(count)) 26 | -------------------------------------------------------------------------------- /documentation/code/enterdebugger.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | while True: 5 | try: 6 | timestamp,input_port,packet = net.recv_packet(timeout=1.0) 7 | except NoPackets: 8 | # timeout waiting for packet arrival 9 | continue 10 | except Shutdown: 11 | # we're done; bail out of while loop 12 | break 13 | 14 | # invoke the debugger every time we get here, which 15 | # should be for every packet we receive! 16 | debugger() 17 | hdrs = packet.num_headers() 18 | 19 | # before exiting our main function, 20 | # perform shutdown on network 21 | net.shutdown() 22 | -------------------------------------------------------------------------------- /examples/readpkt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Simple program that reads contents of a tcpdump tracefile 4 | and prints packets to stdout. 5 | ''' 6 | 7 | import sys 8 | from switchyard.pcapffi import * 9 | from switchyard.lib.packet import * 10 | 11 | files = ['sydump.pcap'] 12 | if len(sys.argv) > 1: 13 | files = sys.argv[1:] 14 | 15 | for infile in files: 16 | print("Opening {}.".format(infile)) 17 | reader = PcapReader(infile) 18 | count = 0 19 | while True: 20 | pkt = reader.recv_packet() 21 | if pkt is None: 22 | break 23 | p = Packet(raw=pkt.raw) 24 | print (p) 25 | count += 1 26 | print ("Got {} packets from {}.".format(count, infile)) 27 | reader.close() 28 | -------------------------------------------------------------------------------- /documentation/code/testscenario1.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | scenario = TestScenario("in/out test scenario example") 4 | 5 | # only one interface on this imaginary device 6 | scenario.add_interface('eth0', 'ab:cd:ef:ab:cd:ef', '1.2.3.4/16', 7 | iftype=InterfaceType.Wired) 8 | 9 | # construct a packet to be received 10 | p = Ethernet(src="00:11:22:33:44:55", dst="66:55:44:33:22:11") + \ 11 | IPv4(src="1.1.1.1", dst="2.2.2.2", protocol=IPProtocol.UDP) + \ 12 | UDP(src=5555, dst=8888) + b'some payload' 13 | 14 | # expect that the packet is received 15 | scenario.expect(PacketInputEvent('eth0', p), 16 | "A udp packet should arrive on eth0") 17 | 18 | # and expect that the packet is sent right back out 19 | scenario.expect(PacketOutputEvent('eth0', p, exact=True), 20 | "The udp packet should be emitted back out eth0") 21 | -------------------------------------------------------------------------------- /switchyard/lib/debugging.py: -------------------------------------------------------------------------------- 1 | # global: use in any timer callbacks 2 | # to decide whether to handle the timer or not. 3 | # if we're in the debugger, just drop it. 4 | 5 | from functools import wraps 6 | import pdb 7 | 8 | in_debugger = False 9 | def disable_timer(): 10 | global in_debugger 11 | in_debugger = True 12 | 13 | 14 | # decorate the "real" debugger entrypoint by 15 | # disabling any SIGALRM invocations -- just ignore 16 | # them if we're going into the debugger 17 | def setup_debugger(f): 18 | @wraps(f) 19 | def wrapper(*args, **kwargs): 20 | disable_timer() 21 | return f(*args, **kwargs) 22 | return wrapper 23 | 24 | @setup_debugger 25 | def debugger(): 26 | '''Invoke the interactive debugger. Can be used anywhere 27 | within a Switchyard program.''' 28 | pdb.Pdb(skip=['switchyard.lib.debugging']).set_trace() 29 | -------------------------------------------------------------------------------- /documentation/code/fullhub.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | # add some informational text about ports on this device 5 | log_info ("Hub is starting up with these ports:") 6 | for port in net.ports(): 7 | log_info ("{}: ethernet address {}".format(port.name, port.ethaddr)) 8 | 9 | while True: 10 | try: 11 | timestamp,input_port,packet = net.recv_packet() 12 | except Shutdown: 13 | # got shutdown signal 14 | break 15 | except NoPackets: 16 | # try again... 17 | continue 18 | 19 | # send the packet out all ports *except* 20 | # the one on which it arrived 21 | for port in net.ports(): 22 | if port.name != input_port: 23 | net.send_packet(port.name, packet) 24 | 25 | # shutdown is the last thing we should do 26 | net.shutdown() 27 | -------------------------------------------------------------------------------- /documentation/code/failhub5.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | # add some informational text about ports on this device 5 | log_info ("Hub is starting up with these ports:") 6 | for port in net.ports(): 7 | log_info ("{}: ethernet address {}".format(port.name, port.ethaddr)) 8 | 9 | while True: 10 | try: 11 | timestamp,input_port,packet = net.recv_packet() 12 | except Shutdown: 13 | # got shutdown signal 14 | break 15 | except NoPackets: 16 | # try again... 17 | continue 18 | 19 | # send the packet out all ports *except* 20 | # the one on which it arrived 21 | for port in net.ports(): 22 | if port.name != input_port: 23 | net.send_packet(port.name, packet) 24 | 25 | # shutdown is the last thing we should do 26 | net.shutdown() 27 | -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | import time 3 | 4 | def main(net): 5 | # assumes that there are exactly 2 ports 6 | portnames = [ p.name for p in net.ports() ] 7 | portpair = dict(zip(portnames, portnames[::-1])) 8 | 9 | while True: 10 | pkt = None 11 | try: 12 | timestamp,input_port,pkt = net.recv_packet(timeout=0.5) 13 | except NoPackets: 14 | pass 15 | except Shutdown: 16 | break 17 | 18 | if pkt is not None: 19 | 20 | # This is logically where you'd include some firewall 21 | # rule tests. It currently just forwards the packet 22 | # out the other port, but depending on the firewall rules 23 | # the packet may be dropped or mutilated. 24 | net.send_packet(portpair[input_port], pkt) 25 | 26 | 27 | net.shutdown() 28 | -------------------------------------------------------------------------------- /examples/sydump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Simple tcpdump-like program that use Switchyard libraries for 4 | reading packets from a local device and dumping them to a file. 5 | ''' 6 | 7 | import sys 8 | from switchyard.pcapffi import * 9 | from switchyard.lib.packet import * 10 | 11 | interface = 'en0' 12 | if len(sys.argv) > 1: 13 | interface = sys.argv[1] 14 | 15 | reader = PcapLiveDevice(interface) 16 | writer = PcapDumper("sydump.pcap") 17 | print("Reading from {}".format(interface)) 18 | count = 0 19 | while True: 20 | pkt = reader.recv_packet(10.0) 21 | if pkt is None: 22 | break 23 | try: 24 | p = Packet(raw=pkt.raw) 25 | print (p) 26 | except Exception as e: 27 | print ("Failed to parse packet: {}".format(e)) 28 | 29 | writer.write_packet(pkt.raw) 30 | count += 1 31 | 32 | print ("Got {} packets".format(count)) 33 | reader.close() 34 | writer.close() 35 | -------------------------------------------------------------------------------- /documentation/code/intfex.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | def main(net): 4 | for intf in net.interfaces(): 5 | addrs = ','.join([str(a) for a in intf.ipaddrs]) 6 | log_info("{} has ethaddr {} and ipaddrs {} and is of type {}".format( 7 | intf.name, intf.ethaddr, addrs, intf.iftype.name)) 8 | 9 | # below, recvdata is a namedtuple 10 | recvdata = net.recv_packet() 11 | print ("At {}, received {} on {}".format( 12 | recvdata.timestamp, recvdata.packet, recvdata.input_port)) 13 | 14 | # alternatively, the above line could use indexing, although 15 | # readability suffers: 16 | # recvdata[0], recvdata[2], recvdata[1])) 17 | 18 | net.send_packet(recvdata.input_port, recvdata.packet) 19 | 20 | # likewise, the above line could be written using indexing 21 | # but, again, readability suffers: 22 | # net.send_packet(recvdata[1], recvdata[2]) 23 | -------------------------------------------------------------------------------- /examples/udpstack_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from copy import deepcopy 4 | from switchyard.lib.userlib import * 5 | 6 | def udp_stack_tests(): 7 | s = TestScenario("UDP stack test (with pretend localhost)") 8 | s.add_interface('lo0', '00:00:00:00:00:00', '127.0.0.1', iftype=InterfaceType.Loopback) 9 | 10 | p = Null() + \ 11 | IPv4(src='127.0.0.1',dst='127.0.0.1',protocol=IPProtocol.UDP) + \ 12 | UDP(src=65535, dst=10000) + b'Hello stack' 13 | 14 | s.expect(PacketOutputEvent("lo0", p, exact=False, wildcards=[(UDP, 'src')]), "Emit UDP packet") 15 | 16 | reply = deepcopy(p) 17 | reply[1].src,reply[1].dst = reply[1].dst,reply[1].src 18 | reply[2].src,reply[2].dst = reply[2].dst,reply[2].src 19 | 20 | s.expect(PacketInputEvent('lo0', reply, 21 | copyfromlastout=('lo0',UDP,'src',UDP,'dst')), 22 | "Receive UDP packet") 23 | 24 | return s 25 | 26 | scenario = udp_stack_tests() 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | before_install: 5 | - sudo apt-get -qq update 6 | - sudo apt-get install -y libffi-dev libpcap-dev 7 | install: 8 | - python setup.py install 9 | script: 10 | - python tests/test_addr.py 11 | - python tests/test_ipv6.py 12 | - python tests/test_ripv2.py 13 | - python tests/test_addr.py 14 | - python tests/test_llnetdev.py 15 | - python tests/test_arp.py 16 | - python tests/test_matcher.py 17 | - python tests/test_scenarios.py 18 | - python tests/test_null.py 19 | - python tests/test_socket.py 20 | - python tests/test_ethernet.py 21 | - python tests/test_srpy.py 22 | - python tests/test_hostfirewall.py 23 | - python tests/test_tcp.py 24 | - python tests/test_icmp.py 25 | # temporarily remove uncritical apis 26 | # - python tests/test_topo.py 27 | - python tests/test_importer.py 28 | - python tests/test_packet.py 29 | - python tests/test_udp.py 30 | - python tests/test_ipv4.py 31 | -------------------------------------------------------------------------------- /switchyard/lib/packet/util.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | def create_ip_arp_reply(srchw, dsthw, srcip, targetip): 4 | ''' 5 | Create an ARP reply (just change what needs to be changed 6 | from a request) 7 | ''' 8 | pkt = create_ip_arp_request(srchw, srcip, targetip) 9 | pkt[0].dst = dsthw 10 | pkt[1].operation = ArpOperation.Reply 11 | pkt[1].targethwaddr = dsthw 12 | return pkt 13 | 14 | def create_ip_arp_request(srchw, srcip, targetip): 15 | ''' 16 | Create and return a packet containing an Ethernet header 17 | and ARP header. 18 | ''' 19 | ether = Ethernet() 20 | ether.src = srchw 21 | ether.dst = SpecialEthAddr.ETHER_BROADCAST.value 22 | ether.ethertype = EtherType.ARP 23 | arp = Arp() 24 | arp.operation = ArpOperation.Request 25 | arp.senderhwaddr = srchw 26 | arp.senderprotoaddr = srcip 27 | arp.targethwaddr = SpecialEthAddr.ETHER_BROADCAST.value 28 | arp.targetprotoaddr = targetip 29 | return ether + arp 30 | -------------------------------------------------------------------------------- /examples/sendpkt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Simple program that uses Switchyard libraries to emit a packet 4 | on every interface that can be opened. 5 | ''' 6 | 7 | from switchyard.lib.userlib import * 8 | 9 | def main(net): 10 | my_interfaces = net.interfaces() 11 | 12 | eth = Ethernet(dst='ff:ff:ff:ff:ff:ff') 13 | ip = IPv4(dst='192.168.100.100', ttl=16, protocol=IPProtocol.ICMP) 14 | icmp = ICMP(icmptype=ICMPType.EchoRequest) 15 | icmp.icmpdata.sequence = 1 16 | icmp.icmpdata.identifier = 13 17 | pkt = eth+ip+icmp 18 | for intf in my_interfaces: 19 | eth.src = intf.ethaddr 20 | # assumes at least one v4 address assigned 21 | v4addrs = [a for a in intf.ipaddrs if a.version == 4] 22 | ip.src = v4addrs[0].src.ip 23 | print("Sending {} out {}".format(pkt, intf.name)) 24 | try: 25 | net.send_packet(intf.name, pkt) 26 | except Exception as e: 27 | log_failure("Can't send packet: {}".format(str(e))) 28 | net.shutdown() 29 | -------------------------------------------------------------------------------- /examples/exercises/router/router4.rst: -------------------------------------------------------------------------------- 1 | Router extensions 2 | ----------------- 3 | 4 | Here are three possible extensions to the router with exercise descriptions: 5 | 6 | * For a firewall extension:,see the ``firewall`` exercise folder. 7 | 8 | * For a RIPv2-like distance vector dynamic routing extension, see the ``dvroute`` folder. 9 | 10 | * For a link state (OSPF-like) dynamic routing extension, see the ``lsroute`` folder. 11 | 12 | Another idea, but without an exercise description: 13 | 14 | * Add a load balancing capability among 2 or more interfaces that are assumed to be connected to links leading to the same far-end destination (either to a single remote router or to multiple co-located routers). You could create a hash function that uses packet header attributes as input to make a decision about which interface on which to forward the packet. 15 | 16 | 17 | License 18 | ------- 19 | 20 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 21 | http://creativecommons.org/licenses/by-nc-sa/4.0/ 22 | -------------------------------------------------------------------------------- /examples/myhub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Ethernet hub in Switchyard. 5 | ''' 6 | from switchyard.lib.userlib import * 7 | 8 | def main(net): 9 | my_interfaces = net.interfaces() 10 | mymacs = [intf.ethaddr for intf in my_interfaces] 11 | 12 | while True: 13 | try: 14 | timestamp,dev,packet = net.recv_packet() 15 | except NoPackets: 16 | continue 17 | except Shutdown: 18 | return 19 | 20 | log_debug ("In {} received packet {} on {}".format(net.name, packet, dev)) 21 | eth = packet.get_header(Ethernet) 22 | if eth is None: 23 | log_info("Received a non-Ethernet packet?!") 24 | continue 25 | 26 | if eth.dst in mymacs: 27 | log_info ("Received a packet intended for me") 28 | else: 29 | for intf in my_interfaces: 30 | if dev != intf.name: 31 | log_info ("Flooding packet {} to {}".format(packet, intf.name)) 32 | net.send_packet(intf, packet) 33 | net.shutdown() 34 | -------------------------------------------------------------------------------- /documentation/code/fullhub2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Ethernet hub in Switchyard. 5 | ''' 6 | from switchyard.lib.userlib import * 7 | 8 | def main(net): 9 | my_interfaces = net.interfaces() 10 | mymacs = [intf.ethaddr for intf in my_interfaces] 11 | 12 | while True: 13 | try: 14 | timestamp,dev,packet = net.recv_packet() 15 | except NoPackets: 16 | continue 17 | except Shutdown: 18 | return 19 | 20 | log_debug ("In {} received packet {} on {}".format(net.name, packet, dev)) 21 | eth = packet.get_header(Ethernet) 22 | if eth is None: 23 | log_info("Received a non-Ethernet packet?!") 24 | continue 25 | 26 | if eth.dst in mymacs: 27 | log_info ("Received a packet intended for me") 28 | else: 29 | for intf in my_interfaces: 30 | if dev != intf.name: 31 | log_info ("Flooding packet {} to {}".format(packet, intf.name)) 32 | net.send_packet(intf, packet) 33 | net.shutdown() 34 | -------------------------------------------------------------------------------- /documentation/thanks.rst: -------------------------------------------------------------------------------- 1 | Acknowledgments and thanks 2 | ************************** 3 | 4 | Thanks to Colgate COSC465 students from Spring 2014 and Spring 2015 for being guinea pigs and giving feedback for the very first versions of Switchyard. Thanks also to Prof. Paul Barford and CS640 students at the University of Wisconsin for using and providing feedback on Switchyard. 5 | 6 | Thanks to those students who have contributed fixes and made suggestions for improvements. In particular: 7 | 8 | * Thanks to Saul Shanabrook for several specific suggestions and bug reports that have led to improvements in Switchyard. 9 | * Thanks to Xuyi Ruan for identifying and suggesting a fix to bugs on one of the documentation diagrams. 10 | * Thanks to Sean Wilson for a bug fix on an infinitely recursive property setter. Oops, but this dumb bug motivated me to significantly improve test coverage, so there's that. 11 | * Thanks to Leon Yang for identifying a problem with kwarg processing for ICMP. 12 | * Thanks to Jordan Ansell at Victoria University-Wellington NZ for initial code for ICMPv6 neighbor discovery 13 | -------------------------------------------------------------------------------- /docs/_sources/thanks.rst.txt: -------------------------------------------------------------------------------- 1 | Acknowledgments and thanks 2 | ************************** 3 | 4 | Thanks to Colgate COSC465 students from Spring 2014 and Spring 2015 for being guinea pigs and giving feedback for the very first versions of Switchyard. Thanks also to Prof. Paul Barford and CS640 students at the University of Wisconsin for using and providing feedback on Switchyard. 5 | 6 | Thanks to those students who have contributed fixes and made suggestions for improvements. In particular: 7 | 8 | * Thanks to Saul Shanabrook for several specific suggestions and bug reports that have led to improvements in Switchyard. 9 | * Thanks to Xuyi Ruan for identifying and suggesting a fix to bugs on one of the documentation diagrams. 10 | * Thanks to Sean Wilson for a bug fix on an infinitely recursive property setter. Oops, but this dumb bug motivated me to significantly improve test coverage, so there's that. 11 | * Thanks to Leon Yang for identifying a problem with kwarg processing for ICMP. 12 | * Thanks to Jordan Ansell at Victoria University-Wellington NZ for initial code for ICMPv6 neighbor discovery 13 | -------------------------------------------------------------------------------- /examples/exercises/learning_switch/myswitch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Ethernet learning switch in Python. 3 | 4 | Note that this file currently has the code to implement a "hub" 5 | in it, not a learning switch. (I.e., it's currently a switch 6 | that doesn't learn.) 7 | ''' 8 | from switchyard.lib.userlib import * 9 | 10 | def main(net): 11 | my_interfaces = net.interfaces() 12 | mymacs = [intf.ethaddr for intf in my_interfaces] 13 | 14 | while True: 15 | try: 16 | timestamp,input_port,packet = net.recv_packet() 17 | except NoPackets: 18 | continue 19 | except Shutdown: 20 | return 21 | 22 | log_debug ("In {} received packet {} on {}".format(net.name, packet, input_port)) 23 | if packet[0].dst in mymacs: 24 | log_debug ("Packet intended for me") 25 | else: 26 | for intf in my_interfaces: 27 | if input_port != intf.name: 28 | log_debug ("Flooding packet {} to {}".format(packet, intf.name)) 29 | net.send_packet(intf.name, packet) 30 | net.shutdown() 31 | -------------------------------------------------------------------------------- /switchyard/lib/exceptions.py: -------------------------------------------------------------------------------- 1 | class SwitchyardException(Exception): 2 | def __init__(self, message): 3 | self.message = message 4 | 5 | def __str__(self): 6 | return self.message 7 | 8 | def __repr__(self): 9 | return self.message 10 | 11 | class Shutdown(SwitchyardException): 12 | '''Exception that is raised in user Switchyard program when the 13 | framework is being shut down.''' 14 | def __init__(self, *args): 15 | SwitchyardException.__init__(self, "Framework shutdown") 16 | 17 | 18 | class NoPackets(SwitchyardException): 19 | '''Exception that is raised in user Switchyard program when 20 | the recv_packet() method is called on the net object and there 21 | are no packets available.''' 22 | def __init__(self, *args): 23 | SwitchyardException.__init__(self, "No packets available") 24 | 25 | class NotEnoughDataError(SwitchyardException): 26 | '''Exception that is raised when attempting to build a packet 27 | header object from a bytes object, but there aren't enough bytes 28 | to perform the reconstruction.''' 29 | pass 30 | -------------------------------------------------------------------------------- /switchyard/sim/linkem.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import heapq 3 | from queue import Queue,Empty 4 | import time 5 | 6 | class LinkEmulator(object): 7 | def __init__(self, inqueue): 8 | self.expiryheap = [] 9 | self.inqueue = inqueue 10 | self.__shutdown = False 11 | 12 | def shutdown(self): 13 | self.__shutdown = True 14 | 15 | def run(self): 16 | while not self.__shutdown: 17 | 18 | now = time.time() 19 | while len(self.expiryheap) and self.expiryheap[0][0] <= now: 20 | expiretime,item,outqueue = heapq.heappop(self.expiryheap) 21 | outqueue.put(item) 22 | 23 | if len(self.expiryheap): 24 | expiretime,item,outqueue = self.expiryheap[0] 25 | timeout = expiretime - time.time() 26 | else: 27 | timeout = 0.1 28 | 29 | try: 30 | expiretime,item,outqueue = self.inqueue.get(timeout=timeout) 31 | except Empty: 32 | pass 33 | else: 34 | heapq.heappush(self.expiryheap, (expiretime, item, outqueue)) 35 | 36 | -------------------------------------------------------------------------------- /documentation/code/baaadhub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Ethernet hub in Switchyard. 5 | ''' 6 | from switchyard.lib.userlib import * 7 | 8 | def main(net): 9 | my_interfaces = net.interfaces() 10 | mymacs = [intf.ethaddr for intf in my_interfaces] 11 | 12 | while True: 13 | try: 14 | timestamp,dev,packet = net.recv_packet() 15 | except NoPackets: 16 | continue 17 | except Shutdown: 18 | return 19 | 20 | log_debug ("In {} received packet {} on {}".format(net.name, packet, dev)) 21 | eth = packet.get_header(Ethernet) 22 | if eth is None: 23 | log_info("Received a non-Ethernet packet?!") 24 | continue 25 | 26 | if eth.dst in mymacs: 27 | log_info ("Received a packet intended for me") 28 | else: 29 | for intf in my_interfaces: 30 | if dev != intf.name: 31 | log_info ("Flooding packet {} to {}".format(packet, intf.name)) 32 | eth.src = 'ba:ba:ba:ba:ba:ba' # sheep! 33 | net.send_packet(intf, packet) 34 | net.shutdown() 35 | -------------------------------------------------------------------------------- /switchyard/lib/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ..textcolor import * 4 | from .debugging import debugger 5 | 6 | def setup_logging(debug, logfile=None): 7 | ''' 8 | Setup logging format and log level. 9 | ''' 10 | if debug: 11 | level = logging.DEBUG 12 | else: 13 | level = logging.INFO 14 | if logfile is not None: 15 | logging.basicConfig(format="%(asctime)s %(levelname)8s %(message)s", datefmt="%H:%M:%S %Y/%m/%d", level=level, filename=logfile) 16 | else: 17 | logging.basicConfig(format="%(asctime)s %(levelname)8s %(message)s", datefmt="%H:%M:%S %Y/%m/%d", level=level) 18 | 19 | def log_failure(s): 20 | '''Convenience function for failure message.''' 21 | with red(): 22 | logging.fatal("{}".format(s)) 23 | 24 | def log_debug(s): 25 | '''Convenience function for debugging message.''' 26 | logging.debug("{}".format(s)) 27 | 28 | def log_warn(s): 29 | '''Convenience function for warning message.''' 30 | with magenta(): 31 | logging.warning("{}".format(s)) 32 | 33 | def log_info(s): 34 | '''Convenience function for info message.''' 35 | logging.info("{}".format(s)) 36 | -------------------------------------------------------------------------------- /examples/exercises/router/myrouter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Basic IPv4 router (static routing) in Python. 5 | ''' 6 | 7 | import sys 8 | import os 9 | import time 10 | from switchyard.lib.userlib import * 11 | 12 | class Router(object): 13 | def __init__(self, net): 14 | self.net = net 15 | # other initialization stuff here 16 | 17 | 18 | def router_main(self): 19 | ''' 20 | Main method for router; we stay in a loop in this method, receiving 21 | packets until the end of time. 22 | ''' 23 | while True: 24 | gotpkt = True 25 | try: 26 | timestamp,dev,pkt = self.net.recv_packet(timeout=1.0) 27 | except NoPackets: 28 | log_debug("No packets available in recv_packet") 29 | gotpkt = False 30 | except Shutdown: 31 | log_debug("Got shutdown signal") 32 | break 33 | 34 | if gotpkt: 35 | log_debug("Got a packet: {}".format(str(pkt))) 36 | 37 | 38 | 39 | def main(net): 40 | ''' 41 | Main entry point for router. Just create Router 42 | object and get it going. 43 | ''' 44 | r = Router(net) 45 | r.router_main() 46 | net.shutdown() 47 | -------------------------------------------------------------------------------- /documentation/code/inouttest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from switchyard.lib.userlib import * 4 | 5 | def mk_pkt(hwsrc, hwdst, ipsrc, ipdst, reply=False): 6 | ether = Ethernet(src=hwsrc, dst=hwdst, ethertype=EtherType.IP) 7 | ippkt = IPv4(src=ipsrc, dst=ipdst, protocol=IPProtocol.ICMP, ttl=32) 8 | icmppkt = ICMP() 9 | if reply: 10 | icmppkt.icmptype = ICMPType.EchoReply 11 | else: 12 | icmppkt.icmptype = ICMPType.EchoRequest 13 | return ether + ippkt + icmppkt 14 | 15 | def inouttests(): 16 | s = TestScenario("in/out basic tests") 17 | s.add_interface('eth0', '10:00:00:00:00:01', '172.16.42.1', '255.255.255.252') 18 | s.add_interface('eth1', '10:00:00:00:00:02', '10.10.0.1', '255.255.0.0') 19 | s.add_interface('eth2', '10:00:00:00:00:03', '192.168.1.1', '255.255.255.0') 20 | 21 | # test case 1: a frame with broadcast destination should get sent out 22 | # all ports except ingress 23 | testpkt = mk_pkt("30:00:00:00:00:02", "ff:ff:ff:ff:ff:ff", "172.16.42.2", "255.255.255.255") 24 | s.expect(PacketInputEvent("eth1", testpkt, display=Ethernet), "An Ethernet frame with a broadcast destination address should arrive on eth1") 25 | s.expect(PacketOutputEvent("eth1", testpkt, display=Ethernet), "The Ethernet frame should be forwarded back out eth1.") 26 | return s 27 | 28 | scenario = inouttests() 29 | -------------------------------------------------------------------------------- /documentation/code/protostackpattern.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | class ProtocolStack(object): 4 | def __init__(self, net): 5 | self._net = net 6 | 7 | def handle_app_data(self, appdata): 8 | # do something to handle application data here, likely 9 | # resulting in an eventual call to self._net.send_packet() 10 | 11 | def handle_network_data(self, netdata): 12 | # do something with network data here, likely resulting 13 | # in an eventual call to ApplicationLayer.send_to_app() 14 | 15 | def main_loop(self): 16 | while True: 17 | appdata = None 18 | try: 19 | appdata = ApplicationLayer.recv_from_app(timeout=0.1) 20 | except NoPackets: 21 | pass 22 | except Shutdown: 23 | break 24 | if appdata is not None: 25 | handle_app_data(net, intf, appdata) 26 | 27 | netdata = None 28 | try: 29 | netdata = net.recv_packet(timeout=0.1) 30 | except NoPackets: 31 | pass 32 | except Shutdown: 33 | break 34 | if netdata is not None: 35 | handle_network_data(netdata) 36 | 37 | 38 | def main(net): 39 | stack = ProtocolStack(net) 40 | stack.main_loop() 41 | net.shutdown() 42 | -------------------------------------------------------------------------------- /tests/test_addr.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | from switchyard.lib.address import * 3 | from ipaddress import AddressValueError 4 | import unittest 5 | 6 | class AddressTests(unittest.TestCase): 7 | def testEthAddr(self): 8 | e = EthAddr() 9 | self.assertEqual(e, SpecialEthAddr.ETHER_ANY.value) 10 | e1 = EthAddr("01-80-C2-00-00-0e") 11 | self.assertTrue(e1.is_bridge_filtered) 12 | e2 = EthAddr("e2-00-00-00-00-00") 13 | self.assertTrue(e2.is_local) 14 | self.assertFalse(e2.is_global) 15 | self.assertEqual(e2.raw, b'\xe2\x00\x00\x00\x00\x00') 16 | self.assertEqual(e2.toRaw(), b'\xe2\x00\x00\x00\x00\x00') 17 | self.assertEqual(e2.packed, b'\xe2\x00\x00\x00\x00\x00') 18 | self.assertEqual(e2.toStr('-'), "e2-00-00-00-00-00") 19 | self.assertEqual(str(e2), "e2:00:00:00:00:00") 20 | self.assertEqual(repr(e2), "EthAddr('e2:00:00:00:00:00')") 21 | self.assertEqual(e2.toTuple(), (0xe2, 0x0, 0x0, 0x0, 0x0, 0x0)) 22 | self.assertTrue(e1 < e2) 23 | 24 | def testSpecialEth(self): 25 | self.assertEqual(SpecialEthAddr.ETHER_ANY.value.raw, b'\x00'*6) 26 | self.assertTrue(SpecialEthAddr.LLDP_MULTICAST.value.is_multicast) 27 | self.assertEqual(str(SpecialEthAddr.PAE_MULTICAST.value), "01:80:c2:00:00:03") 28 | 29 | 30 | if __name__ == '__main__': 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /tests/test_null.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from socket import AF_INET, AF_INET6, AF_DECnet 3 | 4 | from switchyard.lib.packet import * 5 | 6 | class NullPacketTests(unittest.TestCase): 7 | def testNullInstance(self): 8 | n = Null() 9 | b = n.to_bytes() 10 | self.assertEqual(len(b), 4) 11 | self.assertEqual(len(b), n.size()) 12 | self.assertEqual(b, b'\x02\x00\x00\x00') 13 | 14 | n2 = Null() 15 | self.assertEqual(n, n2) 16 | 17 | self.assertEqual(str(n), "Null: AF_INET") 18 | self.assertIsNone(n2.pre_serialize(None, None, None)) 19 | 20 | def testAf(self): 21 | n = Null() 22 | self.assertEqual(n.af, AF_INET) 23 | n.af = AF_INET6 24 | self.assertEqual(n.af, AF_INET6) 25 | self.assertEqual(n.next_header_class(), IPv6) 26 | n.af = AF_INET 27 | self.assertEqual(n.next_header_class(), IPv4) 28 | 29 | n.af = AF_DECnet 30 | with self.assertRaises(Exception): 31 | n.next_header_class() 32 | 33 | def testFromBytes(self): 34 | n = Null(AF_INET6) 35 | b = n.to_bytes() 36 | 37 | with self.assertRaises(Exception): 38 | n.from_bytes(b'\x00') 39 | 40 | x = n.from_bytes(b'\x02\x00\x00\x00') 41 | n2 = Null(AF_INET) 42 | self.assertEqual(n, n2) 43 | 44 | self.assertEqual(x, b'') 45 | 46 | 47 | if __name__ == '__main__': 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /examples/exercises/applayer/msgboardmiddlebox.rst: -------------------------------------------------------------------------------- 1 | Message board application middlebox 2 | ----------------------------------- 3 | 4 | The goal of the third and final part of the exercise is to create a simple middlebox device to introduce packet loss in the network. This part can either be done with "hard-coded" Ethernet address/IPv4 address mappings, or can implement ARP to operate in a more flexible manner. 5 | 6 | You'll implement this device using the Switchyard framework. It will only have two ports, with one port handling traffic to/from a MBclient, and the other port handling traffic to/from a MBserver. When ever a packet arrives on one port, it should be forwarded out the other port, and vice versa. There is no need for any explicit routing. 7 | 8 | Here's the "fun" part: although there's a pretty dumb forwarding mechanism used by the device, it will also be in charge of probabilistically dropping packets to simulate the evil sorts of things that can happen in a real network. Packet drops should only happen in the MBserver to MBclient direction, not the other way around. 9 | 10 | The ``main`` function of your middlebox device should accept, in addition to the ``net`` object, a floating point number between 0 and 1 which represents the probability of dropping a given packet. 11 | 12 | That's it! 13 | 14 | License 15 | ------- 16 | 17 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 18 | http://creativecommons.org/licenses/by-nc-sa/4.0/ 19 | -------------------------------------------------------------------------------- /tests/test_udp.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | import unittest 3 | 4 | class UDPPacketTests(unittest.TestCase): 5 | def testSerialize(self): 6 | ether = Ethernet() 7 | ether.src = '00:00:00:11:22:33' 8 | ether.dst = '11:22:33:00:00:00' 9 | ether.ethertype = EtherType.IP 10 | ippkt = IPv4() 11 | ippkt.src = '1.2.3.4' 12 | ippkt.dst = '4.5.6.7' 13 | ippkt.protocol = IPProtocol.UDP 14 | ippkt.ttl = 37 15 | ippkt.ipid = 0 16 | udppkt = UDP() 17 | udppkt.src = 10000 18 | udppkt.dst = 9999 19 | pkt = ether + ippkt + udppkt + RawPacketContents('hello, world') 20 | b = pkt.to_bytes() 21 | 22 | newpkt = Packet(raw=b) 23 | self.assertEqual(b, newpkt.to_bytes()) 24 | 25 | b = b[:-15] # slice into the udp header 26 | with self.assertRaises(Exception): 27 | newpkt = Packet(raw=b) 28 | 29 | def testChecksum(self): 30 | u = UDP() 31 | self.assertEqual(u.checksum, 0) 32 | 33 | p = IPv4() + u 34 | p[0].protocol = IPProtocol.UDP 35 | b = p.to_bytes() 36 | self.assertEqual(u.checksum, 65502) 37 | u = UDP(src=1234,dst=4567) 38 | u.pre_serialize(b'', Packet(), 0) 39 | self.assertEqual(u.checksum, 0) 40 | 41 | x = b'\x00\x00\xfe\xff\x00\x00' 42 | c = checksum(x, start=0, skip_word=1) 43 | self.assertEqual(c, 65535) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /documentation/code/testscenario2.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | 3 | scenario = TestScenario("packet forwarding example") 4 | 5 | # three interfaces on this device 6 | scenario.add_interface('eth0', 'ab:cd:ef:ab:cd:ef', '1.2.3.4/16') 7 | scenario.add_interface('eth1', '00:11:22:ab:cd:ef', '5.6.7.8/16') 8 | scenario.add_interface('eth2', 'ab:cd:ef:00:11:22', '9.10.11.12/24') 9 | 10 | # add a forwarding table file to be written out when the test 11 | # scenario is executed 12 | scenario.add_file('forwarding_table.txt', ''' 13 | # network subnet-mask next-hop port 14 | 2.0.0.0 255.0.0.0 9.10.11.13 eth2 15 | 3.0.0.0 255.255.0.0 5.6.100.200 eth1 16 | ''') 17 | 18 | 19 | # construct a packet to be received 20 | p = Ethernet(src="00:11:22:33:44:55", dst="66:55:44:33:22:11") + \ 21 | IPv4(src="1.1.1.1", dst="2.2.2.2", protocol=IPProtocol.UDP, ttl=61) + \ 22 | UDP(src=5555, dst=8888) + b'some payload' 23 | 24 | # expect that the packet is received 25 | scenario.expect(PacketInputEvent('eth0', p), 26 | "A udp packet destined to 2.2.2.2 arrives on port eth0") 27 | 28 | # and subsequently forwarded out the correct port; employ 29 | # subset (exact=False) matching, along with a check that the 30 | # IPv4 TTL was decremented exactly by 1. 31 | scenario.expect(PacketOutputEvent('eth2', p, exact=False, 32 | predicate='''lambda pkt: pkt.has_header(IPv4) and pkt[IPv4].ttl == 60'''), 33 | "The udp packet destined to 2.2.2.2 should be forwarded out port eth2, with an appropriately decremented TTL.") 34 | -------------------------------------------------------------------------------- /examples/exercises/applayer/applayer.rst: -------------------------------------------------------------------------------- 1 | Application layer 2 | ----------------- 3 | 4 | Note: these exercises are a work in progress and some details may not be clear or correctly included yet. 5 | 6 | This exercise has three parts. In the first part, the goal is to create a Python socket-based client for a "message board" application. The server with this the client should communicate is already written; only the client needs to be created. This part of the project does *not* require any use of Switchyard --- only the built-in Python ``socket`` module is used. UDP is used as as the transport protocol. 7 | 8 | The goal of the second part of the exercise is to create a UDP-based network stack, which can be used by the client created in part 1 (and also by the server). The network stack will implement a static window-based form of reliable transport. 9 | 10 | The goal of the third and final part of the exercise is to create a simple middlebox device to introduce packet loss in the network. This part can either be done with "hard-coded" Ethernet address/IPv4 address mappings, or can implement ARP to operate in a more flexible manner. 11 | 12 | The three parts of this exercise are described in detail in three separate documents found in this folder: 13 | 14 | * ``msgboardapp.rst`` 15 | 16 | * ``msgboardstack.rst`` 17 | 18 | * ``msgboardmiddlebox.rst`` 19 | 20 | 21 | 22 | License 23 | ------- 24 | 25 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 26 | http://creativecommons.org/licenses/by-nc-sa/4.0/ 27 | -------------------------------------------------------------------------------- /examples/exercises/firewall/firewall_rules.txt: -------------------------------------------------------------------------------- 1 | 2 | # drop everything from an internal subnet which shouldn't be allowed 3 | # to communicate with rest of internet 4 | # rule 1 5 | deny ip src 192.168.42.0/24 dst any 6 | # rule 2 7 | deny ip src any dst 192.168.42.0/24 8 | 9 | # allow traffic to/from an internal web server that should 10 | # be accessible to external hosts 11 | # rule 3 12 | permit tcp src 192.168.13.13 srcport 80 dst any dstport any 13 | # rule 4 14 | permit tcp src any srcport any dst 192.168.13.13 dstport 80 15 | 16 | # allow DNS (udp port 53) traffic in/out of network 17 | # rule 5 18 | permit udp src 192.168.0.0/16 srcport any dst any dstport 53 19 | # rule 6 20 | permit udp src any srcport 53 dst 192.168.0.0/16 dstport any 21 | 22 | # allow internal hosts access to web (tcp ports 80 and 443) 23 | # rate limit http traffic to 100 kB/s (12500 bytes/sec), but 24 | # don't rate limit any encrypted HTTP traffic. 25 | # rule 7 26 | permit tcp src 192.168.0.0/16 srcport any dst any dstport 80 ratelimit 12500 27 | # rule 8 28 | permit tcp src any srcport any dst 172.16.42.0/24 dstport 80 ratelimit 12500 29 | # rule 9 30 | permit tcp src 192.168.0.0/16 srcport any dst any dstport 443 31 | # rule 10 32 | permit tcp src any srcport any dst 172.16.42.0/24 dstport 443 33 | 34 | # permit, but impair certain traffic flows 35 | # rule 11 36 | permit tcp src 192.168.0.0/24 srcport any dst any dstport 8000 impair 37 | 38 | # permit, but rate limit icmp to 100 bytes/sec 39 | # rule 12 40 | permit icmp src any dst any ratelimit 100 41 | 42 | # block everything else 43 | # rule 13 44 | deny ip src any dst any 45 | -------------------------------------------------------------------------------- /documentation/index.rst: -------------------------------------------------------------------------------- 1 | .. Switchyard documentation master file, created by 2 | sphinx-quickstart on Wed Dec 3 22:08:55 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Switchyard documentation 7 | ======================== 8 | 9 | .. Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | writing_a_program 16 | test_execution 17 | test_scenario_creation 18 | live_execution 19 | advanced_api 20 | installation 21 | reference 22 | release_notes 23 | thanks 24 | 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | I gratefully acknowledge support from the NSF. The materials here are based upon work supported by the National Science Foundation under grants CNS-1814537 ("NeTS: Small: RUI: Automating Active Measurement Metadata Collection and Analysis"; 2018-2021) and CNS-1054985 ("CAREER: Expanding the functionality of Internet routers"; 2011-2017). 34 | 35 | Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author and do not necessarily reflect the views of the National Science Foundation. 36 | 37 | .. todolist:: 38 | 39 | License 40 | ======= 41 | 42 | The Switchyard software is distributed under terms of the GNU General Public License, version 3. 43 | 44 | Switchyard's documentation is distributed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-nc-sa/4.0/. 45 | -------------------------------------------------------------------------------- /docs/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. Switchyard documentation master file, created by 2 | sphinx-quickstart on Wed Dec 3 22:08:55 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Switchyard documentation 7 | ======================== 8 | 9 | .. Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | writing_a_program 16 | test_execution 17 | test_scenario_creation 18 | live_execution 19 | advanced_api 20 | installation 21 | reference 22 | release_notes 23 | thanks 24 | 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | I gratefully acknowledge support from the NSF. The materials here are based upon work supported by the National Science Foundation under grants CNS-1814537 ("NeTS: Small: RUI: Automating Active Measurement Metadata Collection and Analysis"; 2018-2021) and CNS-1054985 ("CAREER: Expanding the functionality of Internet routers"; 2011-2017). 34 | 35 | Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author and do not necessarily reflect the views of the National Science Foundation. 36 | 37 | .. todolist:: 38 | 39 | License 40 | ======= 41 | 42 | The Switchyard software is distributed under terms of the GNU General Public License, version 3. 43 | 44 | Switchyard's documentation is distributed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-nc-sa/4.0/. 45 | -------------------------------------------------------------------------------- /tests/test_color.py: -------------------------------------------------------------------------------- 1 | from switchyard.textcolor import green, red, blue, \ 2 | cyan, magenta, yellow, TextColor 3 | 4 | import unittest 5 | 6 | class ColorlibTests(unittest.TestCase): 7 | def testContext(self): 8 | print ("These tests need eyes...") 9 | with green(): 10 | print ("This should be green!") 11 | with red(): 12 | print ("This should be red!") 13 | with blue(): 14 | print ("This should be blue!") 15 | with cyan(): 16 | print ("This should be cyan!") 17 | with magenta(): 18 | print ("This should be magenta!") 19 | with yellow(): 20 | print ("This should be yellow!") 21 | print ("This should not be colored") 22 | 23 | def testOther(self): 24 | TextColor.setup(False) 25 | self.assertTrue(TextColor._SETUP) 26 | with self.assertRaises(Exception): 27 | TextColor() 28 | 29 | def testNoColor(self): 30 | TextColor.setup(True) 31 | print ("These tests need eyes NONE SHOULD BE COLORED ...") 32 | with green(): 33 | print ("This should NOT be green!") 34 | with red(): 35 | print ("This should NOT be red!") 36 | with blue(): 37 | print ("This should NOT be blue!") 38 | with cyan(): 39 | print ("This should NOT be cyan!") 40 | with magenta(): 41 | print ("This should NOT be magenta!") 42 | with yellow(): 43 | print ("This should NOT be yellow!") 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /documentation/code/pkt_construction.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | p = Packet() # construct a packet object 3 | e = Ethernet() # construct Ethernet header 4 | ip = IPv4() # construct IPv4 header 5 | icmp = ICMP() # construct ICMP header 6 | p += e # add eth header to packet 7 | p += ip # add ip header to packet 8 | p += icmp # add icmp header to packet 9 | print (p) 10 | p = Ethernet() + IPv4() + ICMP() 11 | p.num_headers() 12 | len(p) 13 | p.size() 14 | 15 | p[0] 16 | p[0].src 17 | p[0].dst 18 | p[0].dst = "ab:cd:ef:00:11:22" 19 | str(p[0]) 20 | p[0].dst = EthAddr("00:11:22:33:44:55") 21 | str(p[0]) 22 | p[0].ethertype 23 | p[0].ethertype = EtherType.ARP 24 | print (p) 25 | p[0].ethertype = EtherType.IPv4 # set it back to sensible value 26 | 27 | p.has_header(IPv4) 28 | p.get_header_index(IPv4) 29 | str(p[1]) # access by index 30 | str(p[IPv4]) # access by header type 31 | p[IPv4].protocol 32 | p[IPv4].src 33 | p[IPv4].dst 34 | p[IPv4].dst = '149.43.80.13' 35 | 36 | p.has_header(ICMP) 37 | p.get_header_index(ICMP) 38 | p[2] # access by index; notice no conversion to string 39 | p[ICMP] # access by header type 40 | p[ICMP].icmptype 41 | p[ICMP].icmpcode 42 | p[ICMP].icmpdata 43 | icmp.icmpdata.sequence 44 | icmp.icmpdata.identifier 45 | icmp.icmpdata.identifier = 42 46 | icmp.icmpdata.sequence = 13 47 | print (p) 48 | 49 | icmp.icmpdata.data = "hello, world" 50 | print (p) 51 | 52 | p.to_bytes() 53 | 54 | e = Ethernet(src="11:22:33:44:55:66", dst="66:55:44:33:22:11", ethertype=EtherType.IP) 55 | ip = IPv4(src="1.2.3.4", dst="4.3.2.1", protocol=IPProtocol.UDP, ttl=32) 56 | udp = UDP(src=1234, dst=4321) 57 | p = e + ip + udp + b"this is some application payload!" 58 | print(p) 59 | 60 | 61 | -------------------------------------------------------------------------------- /documentation/code/newheader.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | import struct 3 | 4 | class SpanningTreeMessage(PacketHeaderBase): 5 | _PACKFMT = "6sxB" 6 | 7 | def __init__(self, root="00:00:00:00:00:00", **kwargs): 8 | self._root = EthAddr(root) 9 | self._hops_to_root = 0 10 | PacketHeaderBase.__init__(self, **kwargs) 11 | 12 | def to_bytes(self): 13 | raw = struct.pack(self._PACKFMT, self._root.raw, self._hops_to_root) 14 | return raw 15 | 16 | def from_bytes(self, raw): 17 | packsize = struct.calcsize(self._PACKFMT) 18 | if len(raw) < packsize: 19 | raise ValueError("Not enough bytes to unpack SpanningTreeMessage") 20 | xroot,xhops = struct.unpack(self._PACKFMT, raw[:packsize]) 21 | self._root = EthAddr(xroot) 22 | self.hops_to_root = xhops 23 | return raw[packsize:] 24 | 25 | @property 26 | def hops_to_root(self): 27 | return self._hops_to_root 28 | 29 | @hops_to_root.setter 30 | def hops_to_root(self, value): 31 | self._hops_to_root = int(value) 32 | 33 | @property 34 | def root(self): 35 | return self._root 36 | 37 | def __str__(self): 38 | return "{} (root: {}, hops-to-root: {})".format( 39 | self.__class__.__name__, self.root, self.hops_to_root) 40 | 41 | 42 | if __name__ == '__main__': 43 | spm = SpanningTreeMessage("00:11:22:33:44:55", hops_to_root=1) 44 | print(spm) 45 | 46 | Ethernet.add_next_header_class(EtherType.SLOW, SpanningTreeMessage) 47 | pkt = Ethernet(src="11:22:11:22:11:22", 48 | dst="22:33:22:33:22:33", 49 | ethertype=EtherType.SLOW) + spm 50 | print(pkt) 51 | xbytes = pkt.to_bytes() 52 | p = Packet(raw=xbytes) 53 | print(p) 54 | -------------------------------------------------------------------------------- /examples/exercises/README.rst: -------------------------------------------------------------------------------- 1 | Sample Exercises 2 | **************** 3 | 4 | This folder contains sources for sample exercises. Each subfolder includes 5 | a project description and various support files. Note: if you're viewing this source through Github, it will render individual ``.rst`` files if you click on them. If you don't like ReStructuredText, use pandoc to convert to a format you like better. 6 | 7 | **Instructors**: if you'd like the Switchyard test files (and test source code), please email me. Any tests referred to in the project/exercise descriptions are intentionally excluded from this repo (except for the firewall, currently). 8 | 9 | An overview of existing and in-the-works exercises is as follows: 10 | 11 | Learning switch 12 | Build a simple Ethernet learning switch. This is a nice starter exercise for getting accustomed to the APIs and workflow in Switchyard. Some extensions and variants to this exercise are included in the description, such as a spanning-tree protocol-like capability. 13 | 14 | IP router 15 | This is really a set of 3 projects designed to gradually built up capabilities to implement an IPv4 router that uses a static forwarding table. Descriptions of extensions and variants such as dynamic routing are included. 16 | 17 | An exercise to develop an IPv6 router is in progress. 18 | 19 | Firewall 20 | In this exercise, build a stand-alone firewall device with token bucket rate-limiting capability. 21 | 22 | UDP network stack + application 23 | This exercise is an introduction to using Switchyard's socket API emulation capabilities. The goal is to build the Ethernet/IP/UDP layers to support a UDP-based application, along with a basic windowed form of transport reliability. 24 | 25 | License 26 | ------- 27 | 28 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 29 | http://creativecommons.org/licenses/by-nc-sa/4.0/ 30 | -------------------------------------------------------------------------------- /switchyard/lib/packet/null.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import socket 3 | 4 | from .packet import PacketHeaderBase,Packet 5 | from .ipv4 import IPv4 6 | from .ipv6 import IPv6 7 | from ..exceptions import * 8 | 9 | AFTypeClasses = { 10 | socket.AF_INET: IPv4, 11 | socket.AF_INET6: IPv6 12 | } 13 | 14 | AFTypeNames = { 15 | socket.AF_INET: "AF_INET", 16 | socket.AF_INET6: "AF_INET6" 17 | } 18 | 19 | class Null(PacketHeaderBase): 20 | __slots__ = ['_af'] 21 | 22 | def __init__(self, af=socket.AF_INET): 23 | self._af = int(af) 24 | super().__init__() 25 | 26 | def size(self): 27 | return 4 28 | 29 | @property 30 | def af(self): 31 | return self._af 32 | 33 | @af.setter 34 | def af(self,value): 35 | self._af = int(value) 36 | 37 | def to_bytes(self): 38 | ''' 39 | Return packed byte representation of the Ethernet header. 40 | ''' 41 | return struct.pack('=I', self._af) 42 | 43 | def from_bytes(self, raw): 44 | '''Return a Null header object reconstructed from raw bytes, or an 45 | Exception if we can't resurrect the packet.''' 46 | if len(raw) < 4: 47 | raise NotEnoughDataError("Not enough bytes ({}) to reconstruct a Null object".format(len(raw))) 48 | fields = struct.unpack('=I', raw[:4]) 49 | self._af = fields[0] 50 | return raw[4:] 51 | 52 | def next_header_class(self): 53 | cls = AFTypeClasses.get(self.af, None) 54 | if cls is None: 55 | raise Exception("No mapping from address family {} to a packet header class".format(self.af)) 56 | return cls 57 | 58 | def pre_serialize(self, raw, pkt, i): 59 | pass 60 | 61 | def __eq__(self, other): 62 | return self.af == other.af 63 | 64 | def __str__(self): 65 | return '{}: {}'.format(self.__class__.__name__, AFTypeNames.get(self.af, "?")) 66 | 67 | -------------------------------------------------------------------------------- /tests/test_tcp.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | from switchyard.lib.address import * 3 | import unittest 4 | 5 | class TCPPacketTests(unittest.TestCase): 6 | def setUp(self): 7 | self.t = TCP() 8 | 9 | def testReconstruct(self): 10 | self.t.ack = 1234 11 | self.t.ack = 5678 12 | self.t.PSH = True 13 | b = self.t.to_bytes() 14 | t2 = TCP() 15 | t2.from_bytes(b) 16 | self.assertEqual(self.t, t2) 17 | self.assertEqual(t2.size(), 20) 18 | self.assertIsNone(self.t.next_header_class()) 19 | 20 | def testFlags(self): 21 | self.assertEqual(self.t.flags, 0) 22 | self.t.SYN = 1 23 | self.t.FIN = True 24 | self.assertTrue(self.t.SYN) 25 | self.assertTrue(self.t.FIN) 26 | 27 | t2 = TCP(src=40, dst=80, seq=19, ack=47, SYN=1, ACK=1) 28 | self.assertIn("SA", str(t2)) 29 | t2.ACK = 0 30 | self.assertNotIn("SA", str(t2)) 31 | self.assertEqual(t2.ACK, 0) 32 | 33 | for f in TCPFlags: 34 | setattr(t2, f.name, 1) 35 | self.assertEqual(getattr(t2, f.name), 1) 36 | setattr(t2, f.name, 0) 37 | self.assertEqual(getattr(t2, f.name), 0) 38 | 39 | for f in TCPFlags: 40 | setattr(t2, f.name, 1) 41 | self.assertEqual(getattr(t2, f.name), 1) 42 | self.assertEqual(len(t2.flagstr), 9) 43 | 44 | def testBadSet(self): 45 | with self.assertRaises(Exception): 46 | self.t.srcport = 55 47 | 48 | def testChecksum(self): 49 | ip = IPv4(protocol=IPProtocol.TCP) 50 | t = TCP(src=40, dst=80, seq=19, ack=47, SYN=1) 51 | self.assertEqual(t.checksum, 0) 52 | p = Ethernet() + ip + t 53 | b = p.to_bytes() 54 | self.assertEqual("TCP 40->80 (S 19:47)", str(t)) 55 | self.assertEqual(t.checksum, 44841) 56 | 57 | with self.assertRaises(Exception): 58 | x = Packet(raw=b[:-2]) 59 | 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /switchyard/importcode.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import importlib 3 | import os 4 | 5 | from .lib.logging import log_failure, log_debug 6 | 7 | def import_or_die(module_name, entrypoint_names): 8 | ''' 9 | Import user code; return reference to usercode function. 10 | 11 | (str) -> function reference 12 | ''' 13 | log_debug("Importing {}".format(module_name)) 14 | module_name = os.path.abspath(module_name) 15 | if module_name.endswith('.py'): 16 | module_name,ext = os.path.splitext(module_name) 17 | modname = os.path.basename(module_name) 18 | dirname = os.path.dirname(module_name) 19 | if dirname and dirname not in sys.path: 20 | sys.path.append(dirname) 21 | 22 | # first, try to reload code 23 | if modname in sys.modules: 24 | user_module = sys.modules.get(modname) 25 | user_module = importlib.reload(user_module) 26 | # if it isn't in sys.modules, load it for the first time, or 27 | # try to. 28 | else: 29 | try: 30 | mypaths = [ x for x in sys.path if ("Cellar" not in x and "packages" not in x)] 31 | # print("Loading {} from {} ({})".format(modname, dirname, mypaths)) 32 | # user_module = importlib.import_module(modname) 33 | user_module = importlib.__import__(modname) 34 | except ImportError as e: 35 | log_failure("Fatal error: couldn't import module (error: {}) while executing {}".format(str(e), modname)) 36 | raise ImportError(e) 37 | 38 | # if there aren't any functions to call into, then the caller 39 | # just wanted the module/code to be imported, and that's it. 40 | if not entrypoint_names: 41 | return 42 | 43 | existing_names = dir(user_module) 44 | for method in entrypoint_names: 45 | if method in existing_names: 46 | return getattr(user_module, method) 47 | 48 | if len(entrypoint_names) > 1: 49 | entrypoints = "one of {}".format(', '.join(entrypoint_names)) 50 | else: 51 | entrypoints = entrypoint_names[0] 52 | raise ImportError("Required entrypoint function or symbol ({}) not found in your code".format(entrypoints)) 53 | -------------------------------------------------------------------------------- /switchyard/textcolor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import atexit 3 | from contextlib import contextmanager 4 | import colorama 5 | from colorama import Fore, Back, Style 6 | 7 | 8 | class TextColor(object): 9 | _SETUP=False 10 | _NoColor=False 11 | 12 | def __init__(self): 13 | raise Exception("Don't instantiate me.") 14 | 15 | @staticmethod 16 | def setup(nocolor): 17 | if TextColor._SETUP: 18 | return 19 | TextColor._NoColor=nocolor 20 | if sys.platform == 'win32': 21 | colorama.init(autoreset=True,strip=True,convert=True,wrap=True) 22 | else: 23 | colorama.init(autoreset=True,) 24 | atexit.register(TextColor.reset) 25 | TextColor._SETUP=True 26 | 27 | @staticmethod 28 | def reset(): 29 | print(Fore.RESET + Back.RESET + Style.RESET_ALL) 30 | 31 | @staticmethod 32 | def green(): 33 | if not TextColor._NoColor: 34 | print(Fore.GREEN,end='') 35 | 36 | @staticmethod 37 | def red(): 38 | if not TextColor._NoColor: 39 | print(Fore.RED,end='') 40 | 41 | @staticmethod 42 | def blue(): 43 | if not TextColor._NoColor: 44 | print(Fore.BLUE,end='') 45 | 46 | @staticmethod 47 | def cyan(): 48 | if not TextColor._NoColor: 49 | print(Fore.CYAN,end='') 50 | 51 | @staticmethod 52 | def magenta(): 53 | if not TextColor._NoColor: 54 | print(Fore.MAGENTA,end='') 55 | 56 | @staticmethod 57 | def yellow(): 58 | if not TextColor._NoColor: 59 | print(Fore.YELLOW,end='') 60 | 61 | @contextmanager 62 | def red(): 63 | TextColor.red() 64 | yield 65 | TextColor.reset() 66 | 67 | @contextmanager 68 | def green(): 69 | TextColor.green() 70 | yield 71 | TextColor.reset() 72 | 73 | @contextmanager 74 | def blue(): 75 | TextColor.blue() 76 | yield 77 | TextColor.reset() 78 | 79 | @contextmanager 80 | def cyan(): 81 | TextColor.cyan() 82 | yield 83 | TextColor.reset() 84 | 85 | @contextmanager 86 | def magenta(): 87 | TextColor.magenta() 88 | yield 89 | TextColor.reset() 90 | 91 | @contextmanager 92 | def yellow(): 93 | TextColor.yellow() 94 | yield 95 | TextColor.reset() 96 | 97 | -------------------------------------------------------------------------------- /examples/udpstack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from switchyard.lib.userlib import * 4 | 5 | def main(net): 6 | intf = None 7 | 8 | for i in net.interfaces(): 9 | if i.iftype == InterfaceType.Loopback: 10 | intf = i 11 | break 12 | if intf is None: 13 | raise Exception("This example is designed to use the loopback interface but I didn't find one") 14 | 15 | while True: 16 | appdata = None 17 | try: 18 | appdata = ApplicationLayer.recv_from_app(timeout=0.1) 19 | except NoPackets: 20 | pass 21 | except Shutdown: 22 | break 23 | if appdata is not None: 24 | handle_app_data(net, intf, appdata) 25 | 26 | netdata = None 27 | try: 28 | netdata = net.recv_packet(timeout=0.1) 29 | except NoPackets: 30 | pass 31 | except Shutdown: 32 | break 33 | if netdata is not None: 34 | handle_network_data(netdata) 35 | 36 | net.shutdown() 37 | 38 | def handle_app_data(net, intf, appdata): 39 | flowaddr,message = appdata 40 | log_debug("Received data from app layer: <{}>".format(message)) 41 | log_debug("flowaddr: {}".format(flowaddr)) 42 | 43 | proto,srcip,srcport,dstip,dstport = flowaddr 44 | srcaddr = [a for a in intf.ipaddrs if a.version == 4][0] 45 | p = Null() + IPv4(protocol=proto, src=srcaddr.ip, dst=dstip, ipid=0xabcd, ttl=64, flags=IPFragmentFlag.DontFragment) + UDP(src=srcport, dst=dstport) + message 46 | 47 | log_debug("Sending {} to {}".format(p, intf.name)) 48 | net.send_packet(intf, p) 49 | 50 | def handle_network_data(netdata): 51 | timestamp, ingress, pkt = netdata 52 | log_debug("On {} received {}".format(ingress, pkt)) 53 | if pkt.has_header(IPv4): 54 | ipidx = pkt.get_header_index(IPv4) 55 | ip = pkt[ipidx] 56 | if pkt[ipidx].protocol == IPProtocol.UDP: 57 | udp = pkt.get_header(UDP) 58 | ApplicationLayer.send_to_app(IPProtocol.UDP, (ip.dst, udp.dst), 59 | (ip.src, udp.src), pkt[-1].data) 60 | elif pkt[ipidx].protocol == IPProtocol.ICMP: 61 | log_info("Received ICMP message: {}".format(pkt[ipidx+1])) 62 | else: 63 | log_info("Received an unexpected packet: {}".format(pkt[1:])) 64 | 65 | -------------------------------------------------------------------------------- /documentation/code/udpappheader.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.userlib import * 2 | import struct 3 | 4 | class UDPPing(PacketHeaderBase): 5 | _PACKFMT = "!H" 6 | 7 | def __init__(self, seq=0, **kwargs): 8 | self._sequence = int(seq) 9 | PacketHeaderBase.__init__(self, **kwargs) 10 | 11 | def to_bytes(self): 12 | raw = struct.pack(self._PACKFMT, self._sequence) 13 | return raw 14 | 15 | def from_bytes(self, raw): 16 | packsize = struct.calcsize(self._PACKFMT) 17 | if len(raw) < packsize: 18 | raise ValueError("Not enough bytes to unpack UDPPing") 19 | attrs = struct.unpack(self._PACKFMT, raw[:packsize]) 20 | self.sequence = attrs[0] 21 | return raw[packsize:] 22 | 23 | @property 24 | def sequence(self): 25 | return self._sequence 26 | 27 | @sequence.setter 28 | def sequence(self, value): 29 | self._sequence = int(value) 30 | 31 | def __str__(self): 32 | return "{} seq: {}".format(self.__class__.__name__, self.sequence) 33 | 34 | 35 | if __name__ == '__main__': 36 | up1 = UDPPing() 37 | print(up1) 38 | 39 | up2 = UDPPing() 40 | up2.sequence = 13 41 | print(up2) 42 | 43 | up3 = UDPPing(sequence=42) 44 | print(up3) 45 | 46 | UDP_PING_PORT = 12345 47 | pkt = Ethernet(src="11:22:11:22:11:22", 48 | dst="22:33:22:33:22:33") + \ 49 | IPv4(src="1.2.3.4", dst="5.6.7.8", 50 | protocol=IPProtocol.UDP, ttl=64) + \ 51 | UDP(src=55555, dst=UDP_PING_PORT) + \ 52 | UDPPing(42) 53 | print("Before serialize/deserialize:", pkt) 54 | xbytes = pkt.to_bytes() 55 | reanimated_pkt = Packet(raw=xbytes) 56 | print("After deserialization:", reanimated_pkt) 57 | 58 | print("*" * 40) 59 | 60 | UDP.add_next_header_class(UDP_PING_PORT, UDPPing) 61 | UDP.set_next_header_class_key('dst') 62 | pkt = Ethernet(src="11:22:11:22:11:22", 63 | dst="22:33:22:33:22:33") + \ 64 | IPv4(src="1.2.3.4", dst="5.6.7.8", 65 | protocol=IPProtocol.UDP, ttl=64) + \ 66 | UDP(src=55555, dst=UDP_PING_PORT) + \ 67 | UDPPing(sequence=13) 68 | print("Before serialize/deserialize:", pkt) 69 | xbytes = pkt.to_bytes() 70 | reanimated_pkt = Packet(raw=xbytes) 71 | print("After deserialization:", reanimated_pkt) 72 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | thisversion = '1.0.1' 4 | 5 | setup(name="switchyard", 6 | version=thisversion, 7 | description="Switchyard is a framework for creating networked systems", 8 | author="Joel Sommers", 9 | author_email="jsommers@colgate.edu", 10 | url="https://github.com/jsommers/switchyard", 11 | keywords=['education', 'networked systems',], 12 | zip_safe=True, 13 | packages=find_packages(), 14 | python_requires='>=3.6', 15 | package_data={ '': ['*.txt', '*.rst'], }, 16 | exclude_package_data={'': ['README.rst','README.md']}, 17 | install_requires=["cffi >=1.14.0","colorama >=0.4.3","networkx >=2.4", "psutil >=5.7.0"], 18 | tests_require=['coverage >=5.1'], 19 | entry_points= { 20 | 'console_scripts': [ 'swyard = switchyard.swyard:main' ], 21 | }, 22 | license="This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. http://creativecommons.org/licenses/by-nc-sa/4.0/", 23 | classifiers=[ 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Development Status :: 4 - Beta", 26 | "Topic :: Scientific/Engineering", 27 | "Topic :: Education", 28 | "Topic :: Software Development :: Libraries", 29 | "Topic :: System :: Networking", 30 | "Environment :: Console", 31 | "Intended Audience :: Education", 32 | "Intended Audience :: Science/Research", 33 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 34 | ], 35 | long_description=''' 36 | Switchyard is a library and framework for creating networked systems in Python. It is primarily intended for educational use and supports creating devices from layer 2 (Ethernet) all the way through the application layer. 37 | 38 | Documentation is available at http://jsommers.github.io/switchyard 39 | Documentation is written using the Python Sphinx package; doc sources are 40 | available in the documentation directory. 41 | 42 | The Switchyard software is distributed under terms of the GNU General Public License, version 3. 43 | 44 | Switchyard's documentation is distributed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-nc-sa/4.0/. 45 | ''' 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_ethernet.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | from switchyard.lib.address import EthAddr 3 | import unittest 4 | 5 | class EthernetPacketTests(unittest.TestCase): 6 | def setUp(self): 7 | self.e = Ethernet() 8 | 9 | def testBlankAddrs(self): 10 | self.assertEqual(self.e.src, EthAddr()) 11 | self.assertEqual(self.e.dst, EthAddr()) 12 | 13 | def testSetSrc(self): 14 | astr = '00:00:00:00:00:01' 15 | self.e.src = astr 16 | self.assertEqual(self.e.src, EthAddr(astr)) 17 | 18 | def testSetDst(self): 19 | astr = '00:00:00:00:00:01' 20 | self.e.dst = astr 21 | self.assertEqual(self.e.dst, EthAddr(astr)) 22 | 23 | def testBadSet(self): 24 | with self.assertRaises(Exception): 25 | self.e.xdst = EthAddr() 26 | 27 | def testBadEType(self): 28 | # try to set an invalid ethertype 29 | with self.assertRaises(ValueError): 30 | self.e.ethertype = 0x01 31 | 32 | def testBadAddr(self): 33 | with self.assertRaises(RuntimeError): 34 | x = EthAddr("a") 35 | 36 | def testParse(self): 37 | raw = b'\x01\x02\x03\x04\x05\x06\x06\x05\x04\x03\x02\x01\x08\x00' 38 | astr = '01:02:03:04:05:06' 39 | e = Ethernet() 40 | e.from_bytes(raw) 41 | self.assertEqual(e.dst, EthAddr(astr)) 42 | self.assertEqual(e.src, EthAddr(':'.join(astr.split(':')[::-1]))) 43 | self.assertEqual(e.ethertype, EtherType.IP) 44 | 45 | def testBadParse(self): 46 | raw = b'x\01' 47 | e = Ethernet() 48 | with self.assertRaises(Exception): 49 | e.from_bytes(raw) 50 | 51 | def testVlan(self): 52 | e = Ethernet() 53 | e.src = "00:11:22:33:44:55" 54 | e.dst = "aa:bb:cc:dd:ee:ff" 55 | e.ethertype = EtherType.x8021Q 56 | 57 | v = Vlan() 58 | v.vlanid = 42 59 | v.pcp = 2 60 | v.ethertype = EtherType.IP 61 | 62 | ip = IPv4() 63 | ip.src = "1.2.3.4" 64 | ip.dst = "5.6.7.8" 65 | icmp = ICMP() 66 | 67 | packet = e + v + ip + icmp 68 | self.assertEqual(packet.num_headers(), 4) 69 | self.assertEqual(packet[1].vlanid, 42) 70 | self.assertEqual(packet[1].pcp, 2) 71 | self.assertEqual(len(packet), 46) 72 | serialized = packet.to_bytes() 73 | 74 | other = Packet(raw=serialized) 75 | self.assertEqual(packet, other) 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /documentation/code/hubtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from switchyard.lib.userlib import * 4 | 5 | def mk_pkt(hwsrc, hwdst, ipsrc, ipdst, reply=False): 6 | ether = Ethernet(src=hwsrc, dst=hwdst, ethertype=EtherType.IP) 7 | ippkt = IPv4(src=ipsrc, dst=ipdst, protocol=IPProtocol.ICMP, ttl=32) 8 | icmppkt = ICMP() 9 | if reply: 10 | icmppkt.icmptype = ICMPType.EchoReply 11 | else: 12 | icmppkt.icmptype = ICMPType.EchoRequest 13 | return ether + ippkt + icmppkt 14 | 15 | def hub_tests(): 16 | s = TestScenario("hub tests") 17 | s.add_interface('eth0', '10:00:00:00:00:01') 18 | s.add_interface('eth1', '10:00:00:00:00:02') 19 | s.add_interface('eth2', '10:00:00:00:00:03') 20 | 21 | # test case 1: a frame with broadcast destination should get sent out 22 | # all ports except ingress 23 | testpkt = mk_pkt("30:00:00:00:00:02", "ff:ff:ff:ff:ff:ff", "172.16.42.2", "255.255.255.255") 24 | s.expect(PacketInputEvent("eth1", testpkt), "An Ethernet frame with a broadcast destination address should arrive on eth1") 25 | s.expect(PacketOutputEvent("eth0", testpkt, "eth2", testpkt), "The Ethernet frame with a broadcast destination address should be forwarded out ports eth0 and eth2") 26 | 27 | # test case 2: a frame with any unicast address except one assigned to hub 28 | # interface should be sent out all ports except ingress 29 | reqpkt = mk_pkt("20:00:00:00:00:01", "30:00:00:00:00:02", '192.168.1.100','172.16.42.2') 30 | s.expect(PacketInputEvent("eth0", reqpkt), "An Ethernet frame from 20:00:00:00:00:01 to 30:00:00:00:00:02 should arrive on eth0") 31 | s.expect(PacketOutputEvent("eth1", reqpkt, "eth2", reqpkt), "Ethernet frame destined for 30:00:00:00:00:02 should be flooded out eth1 and eth2") 32 | 33 | resppkt = mk_pkt("30:00:00:00:00:02", "20:00:00:00:00:01", '172.16.42.2', '192.168.1.100', reply=True) 34 | s.expect(PacketInputEvent("eth1", resppkt), "An Ethernet frame from 30:00:00:00:00:02 to 20:00:00:00:00:01 should arrive on eth1") 35 | s.expect(PacketOutputEvent("eth0", resppkt, "eth2", resppkt), "Ethernet frame destined to 20:00:00:00:00:01 should be flooded out eth0 and eth2") 36 | 37 | # test case 3: a frame with dest address of one of the interfaces should 38 | # result in nothing happening 39 | reqpkt = mk_pkt("20:00:00:00:00:01", "10:00:00:00:00:03", '192.168.1.100','172.16.42.2') 40 | s.expect(PacketInputEvent("eth2", reqpkt), "An Ethernet frame should arrive on eth2 with destination address the same as eth2's MAC address") 41 | s.expect(PacketInputTimeoutEvent(1.0), "The hub should not do anything in response to a frame arriving with a destination address referring to the hub itself.") 42 | return s 43 | 44 | scenario = hub_tests() 45 | -------------------------------------------------------------------------------- /tests/test_importer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import tempfile 5 | import importlib 6 | 7 | import switchyard.importcode as imp 8 | 9 | class TestImporter(unittest.TestCase): 10 | def _writeFile(self, name): 11 | with open(name, "w") as outf: 12 | print("x = 1", file=outf) 13 | print("def fn():", file=outf) 14 | print(" print(x)", file=outf) 15 | 16 | def setUp(self): 17 | importlib.invalidate_caches() 18 | 19 | def testImporter1(self): 20 | name = "firsttest.py" 21 | self._writeFile(name) 22 | mod = imp.import_or_die(name, None) 23 | self.assertIsNone(mod) 24 | self.assertIn(name[:-3], sys.modules) 25 | os.unlink(name) 26 | 27 | def testImporter1b(self): 28 | name = "firsttwo_partdau.py" 29 | self._writeFile(name) 30 | mod = imp.import_or_die(name[:-3], None) 31 | self.assertIsNone(mod) 32 | self.assertIn(name[:-3], sys.modules) 33 | os.unlink(name) 34 | 35 | def testImporter2(self): 36 | name = "testimp2.py" 37 | self._writeFile(name) 38 | xfn = imp.import_or_die(name, ["fn"]) 39 | self.assertIsNotNone(xfn) 40 | self.assertEqual(xfn.__name__, "fn") 41 | self.assertIn(name[:-3], sys.modules) 42 | os.unlink(name) 43 | 44 | def testImporter3(self): 45 | name = "testimp3.py" 46 | self._writeFile(name) 47 | with self.assertRaises(ImportError): 48 | imp.import_or_die(name, ["ugh"]) 49 | os.unlink(name) 50 | 51 | def testImporter4(self): 52 | name = "testimp4.py" 53 | xfile = os.path.join(tempfile.gettempdir(), name) 54 | self._writeFile(xfile) 55 | xfn = imp.import_or_die(xfile, ["main","blob","fn"]) 56 | self.assertIsNotNone(xfn) 57 | self.assertEqual(xfn.__name__, "fn") 58 | self.assertIn(name[:-3], sys.modules) 59 | os.unlink(xfile) 60 | 61 | def testImporter5(self): 62 | with self.assertLogs() as cm: 63 | with self.assertRaises(ImportError): 64 | imp.import_or_die("/tmp/notafile.py", None) 65 | self.assertIn("couldn't import module", cm.output[0]) 66 | with self.assertLogs() as cm: 67 | with self.assertRaises(ImportError): 68 | imp.import_or_die("nothinghere.py", None) 69 | self.assertIn("couldn't import module", cm.output[0]) 70 | with self.assertLogs() as cm: 71 | with self.assertRaises(ImportError): 72 | imp.import_or_die("nothinghere", None) 73 | self.assertIn("couldn't import module", cm.output[0]) 74 | 75 | 76 | if __name__ == '__main__': 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /tests/test_arp.py: -------------------------------------------------------------------------------- 1 | from switchyard.lib.packet import * 2 | from switchyard.lib.address import EthAddr, ip_address 3 | import unittest 4 | 5 | class ArpTests(unittest.TestCase): 6 | def testArpRequest(self): 7 | p = create_ip_arp_request("00:00:00:11:22:33", "1.2.3.4", "10.11.12.13") 8 | self.assertEqual(p.num_headers(), 2) 9 | ether = p[0] 10 | arp = p[1] 11 | self.assertEqual(ether.src, "00:00:00:11:22:33") 12 | self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") 13 | self.assertEqual(ether.ethertype, EtherType.ARP) 14 | self.assertEqual(len(ether), 14) 15 | self.assertEqual(arp.operation, ArpOperation.Request) 16 | self.assertEqual(arp.hardwaretype, ArpHwType.Ethernet) 17 | self.assertEqual(arp.protocoltype, EtherType.IP) 18 | self.assertEqual(arp.senderhwaddr, EthAddr("00:00:00:11:22:33")) 19 | self.assertEqual(arp.targethwaddr, EthAddr("ff:ff:ff:ff:ff:ff")) 20 | self.assertEqual(arp.senderprotoaddr, IPv4Address("1.2.3.4")) 21 | self.assertEqual(arp.targetprotoaddr, IPv4Address("10.11.12.13")) 22 | serialized = arp.to_bytes() 23 | other = Arp() 24 | other.from_bytes(serialized) 25 | self.assertEqual(arp, other) 26 | self.assertEqual(len(arp), 28) 27 | with self.assertRaises(Exception): 28 | other.from_bytes(serialized[:-3]) 29 | xbytes = arp.to_bytes() 30 | # inject an invalid arp operation 31 | xbytes = xbytes[:6] + b'\xff\xff' + xbytes[8:] 32 | a = Arp() 33 | with self.assertRaises(Exception): 34 | a.from_bytes(xbytes) 35 | 36 | def testArpReply(self): 37 | p = create_ip_arp_reply("aa:bb:cc:dd:ee:ff", "00:00:00:11:22:33", "10.11.12.13", "1.2.3.4") 38 | self.assertEqual(p.num_headers(), 2) 39 | ether = p[0] 40 | arp = p[1] 41 | self.assertEqual(ether.dst, "00:00:00:11:22:33") 42 | self.assertEqual(ether.src, "aa:bb:cc:dd:ee:ff") 43 | self.assertEqual(ether.ethertype, EtherType.ARP) 44 | self.assertEqual(len(ether), 14) 45 | self.assertEqual(arp.operation, ArpOperation.Reply) 46 | self.assertEqual(arp.hardwaretype, ArpHwType.Ethernet) 47 | self.assertEqual(arp.protocoltype, EtherType.IP) 48 | self.assertEqual(arp.targethwaddr, EthAddr("00:00:00:11:22:33")) 49 | self.assertEqual(arp.senderhwaddr, EthAddr("aa:bb:cc:dd:ee:ff")) 50 | self.assertEqual(arp.targetprotoaddr, IPv4Address("1.2.3.4")) 51 | self.assertEqual(arp.senderprotoaddr, IPv4Address("10.11.12.13")) 52 | serialized = arp.to_bytes() 53 | other = Arp() 54 | other.from_bytes(serialized) 55 | self.assertEqual(arp, other) 56 | self.assertEqual(len(arp), 28) 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /examples/hubtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from switchyard.lib.userlib import * 4 | 5 | def mk_pkt(hwsrc, hwdst, ipsrc, ipdst, reply=False): 6 | ether = Ethernet(src=hwsrc, dst=hwdst, ethertype=EtherType.IP) 7 | ippkt = IPv4(src=ipsrc, dst=ipdst, protocol=IPProtocol.ICMP, ttl=32) 8 | icmppkt = ICMP() 9 | if reply: 10 | icmppkt.icmptype = ICMPType.EchoReply 11 | else: 12 | icmppkt.icmptype = ICMPType.EchoRequest 13 | return ether + ippkt + icmppkt 14 | 15 | def hub_tests(): 16 | s = TestScenario("hub tests") 17 | s.add_interface('eth0', '10:00:00:00:00:01') 18 | s.add_interface('eth1', '10:00:00:00:00:02') 19 | s.add_interface('eth2', '10:00:00:00:00:03') 20 | 21 | # test case 1: a frame with broadcast destination should get sent out 22 | # all ports except ingress 23 | testpkt = mk_pkt("30:00:00:00:00:02", "ff:ff:ff:ff:ff:ff", "172.16.42.2", "255.255.255.255") 24 | s.expect(PacketInputEvent("eth1", testpkt, display=Ethernet), "An Ethernet frame with a broadcast destination address should arrive on eth1") 25 | s.expect(PacketOutputEvent("eth0", testpkt, "eth2", testpkt, display=Ethernet), "The Ethernet frame with a broadcast destination address should be forwarded out ports eth0 and eth2") 26 | 27 | # test case 2: a frame with any unicast address except one assigned to hub 28 | # interface should be sent out all ports except ingress 29 | reqpkt = mk_pkt("20:00:00:00:00:01", "30:00:00:00:00:02", '192.168.1.100','172.16.42.2') 30 | s.expect(PacketInputEvent("eth0", reqpkt, display=Ethernet), "An Ethernet frame from 20:00:00:00:00:01 to 30:00:00:00:00:02 should arrive on eth0") 31 | s.expect(PacketOutputEvent("eth1", reqpkt, "eth2", reqpkt, display=Ethernet), "Ethernet frame destined for 30:00:00:00:00:02 should be flooded out eth1 and eth2") 32 | 33 | resppkt = mk_pkt("30:00:00:00:00:02", "20:00:00:00:00:01", '172.16.42.2', '192.168.1.100', reply=True) 34 | s.expect(PacketInputEvent("eth1", resppkt, display=Ethernet), "An Ethernet frame from 30:00:00:00:00:02 to 20:00:00:00:00:01 should arrive on eth1") 35 | s.expect(PacketOutputEvent("eth0", resppkt, "eth2", resppkt, display=Ethernet), "Ethernet frame destined to 20:00:00:00:00:01 should be flooded out eth0 and eth2") 36 | 37 | # test case 3: a frame with dest address of one of the interfaces should 38 | # result in nothing happening 39 | reqpkt = mk_pkt("20:00:00:00:00:01", "10:00:00:00:00:03", '192.168.1.100','172.16.42.2') 40 | s.expect(PacketInputEvent("eth2", reqpkt, display=Ethernet), "An Ethernet frame should arrive on eth2 with destination address the same as eth2's MAC address") 41 | s.expect(PacketInputTimeoutEvent(1.0), "The hub should not do anything in response to a frame arriving with a destination address referring to the hub itself.") 42 | return s 43 | 44 | scenario = hub_tests() 45 | -------------------------------------------------------------------------------- /switchyard/lib/packet/udp.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from .packet import PacketHeaderBase 4 | from .common import checksum 5 | from ..exceptions import * 6 | 7 | ''' 8 | References: 9 | IETF RFC 768 10 | ''' 11 | 12 | # FIXME: checksum is broken for ip6 13 | 14 | class UDP(PacketHeaderBase): 15 | __slots__ = ['_src','_dst','_len','_checksum'] 16 | _PACKFMT = '!HHHH' 17 | _MINLEN = struct.calcsize(_PACKFMT) 18 | _next_header_map = {} 19 | _next_header_class_key = '' 20 | 21 | def __init__(self, **kwargs): 22 | self.src = self.dst = 0 23 | self._len = self.size() 24 | self._checksum = 0 25 | super().__init__(**kwargs) 26 | 27 | def size(self): 28 | return struct.calcsize(UDP._PACKFMT) 29 | 30 | def to_bytes(self): 31 | ''' 32 | Return packed byte representation of the UDP header. 33 | ''' 34 | return struct.pack(UDP._PACKFMT, self._src, self._dst, 35 | self._len, self._checksum) 36 | 37 | def from_bytes(self, raw): 38 | '''Return an Ethernet object reconstructed from raw bytes, or an 39 | Exception if we can't resurrect the packet.''' 40 | if len(raw) < UDP._MINLEN: 41 | raise NotEnoughDataError("Not enough bytes ({}) to reconstruct an UDP object".format(len(raw))) 42 | fields = struct.unpack(UDP._PACKFMT, raw[:UDP._MINLEN]) 43 | self._src = fields[0] 44 | self._dst = fields[1] 45 | self._len = fields[2] 46 | self._checksum = fields[3] 47 | return raw[UDP._MINLEN:] 48 | 49 | def __eq__(self, other): 50 | return self.src == other.src and \ 51 | self.dst == other.dst 52 | 53 | @property 54 | def src(self): 55 | return self._src 56 | 57 | @property 58 | def dst(self): 59 | return self._dst 60 | 61 | @src.setter 62 | def src(self,value): 63 | self._src = value 64 | 65 | @dst.setter 66 | def dst(self,value): 67 | self._dst = value 68 | 69 | @property 70 | def checksum(self): 71 | return self._checksum 72 | 73 | @property 74 | def length(self): 75 | return self._len 76 | 77 | def __str__(self): 78 | return '{} {}->{}'.format(self.__class__.__name__, self.src, self.dst) 79 | 80 | def _compute_checksum_ipv4(self, ip4, xdata): 81 | if ip4 is None: 82 | return 0 83 | xhdr = struct.pack('!IIxBHHHHH', int(ip4.src), int(ip4.dst), 84 | ip4.protocol.value, self._len, 85 | self.src, self.dst, self._len, 0) 86 | return checksum(xhdr + xdata) 87 | 88 | def pre_serialize(self, raw, pkt, i): 89 | self._len = self.size() + len(raw) 90 | # checksum calc currently assumes we're only dealing with ipv4. 91 | # will need to be modified for ipv6 support... 92 | self._checksum = self._compute_checksum_ipv4(pkt.get_header_by_name('IPv4'), raw) 93 | -------------------------------------------------------------------------------- /examples/exercises/learning_switch/switchtopo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | from mininet.topo import Topo 6 | from mininet.net import Mininet 7 | from mininet.log import lg 8 | from mininet.node import CPULimitedHost 9 | from mininet.link import TCLink 10 | from mininet.util import irange, custom, quietRun, dumpNetConnections 11 | from mininet.cli import CLI 12 | 13 | from time import sleep, time 14 | from subprocess import Popen, PIPE 15 | import subprocess 16 | import argparse 17 | import os 18 | 19 | parser = argparse.ArgumentParser(description="Mininet pyswitch topology") 20 | # no arguments needed as yet :-) 21 | args = parser.parse_args() 22 | lg.setLogLevel('info') 23 | 24 | class PySwitchTopo(Topo): 25 | 26 | def __init__(self, args): 27 | # Add default members to class. 28 | super(PySwitchTopo, self).__init__() 29 | 30 | # Host and link configuration 31 | # 32 | # 33 | # server1 34 | # \ 35 | # switch----client 36 | # / 37 | # server2 38 | # 39 | 40 | nodeconfig = {'cpu':-1} 41 | self.addHost('server1', **nodeconfig) 42 | self.addHost('server2', **nodeconfig) 43 | self.addHost('switch', **nodeconfig) 44 | self.addHost('client', **nodeconfig) 45 | 46 | for node in ['server1','server2','client']: 47 | # all links are 10Mb/s, 100 millisecond prop delay 48 | self.addLink(node, 'switch', bw=10, delay='100ms') 49 | 50 | def set_ip(net, node1, node2, ip): 51 | node1 = net.get(node1) 52 | ilist = node1.connectionsTo(net.get(node2)) # returns list of tuples 53 | intf = ilist[0] 54 | intf[0].setIP(ip) 55 | 56 | def reset_macs(net, node, macbase): 57 | ifnum = 1 58 | node_object = net.get(node) 59 | for intf in node_object.intfList(): 60 | node_object.setMAC(macbase.format(ifnum), intf) 61 | ifnum += 1 62 | 63 | for intf in node_object.intfList(): 64 | print node,intf,node_object.MAC(intf) 65 | 66 | def set_route(net, fromnode, prefix, nextnode): 67 | node_object = net.get(fromnode) 68 | ilist = node_object.connectionsTo(net.get(nextnode)) 69 | node_object.setDefaultRoute(ilist[0][0]) 70 | 71 | def setup_addressing(net): 72 | reset_macs(net, 'server1', '10:00:00:00:00:{:02x}') 73 | reset_macs(net, 'server2', '20:00:00:00:00:{:02x}') 74 | reset_macs(net, 'client', '30:00:00:00:00:{:02x}') 75 | reset_macs(net, 'switch', '40:00:00:00:00:{:02x}') 76 | set_ip(net, 'server1','switch','192.168.100.1/24') 77 | set_ip(net, 'server2','switch','192.168.100.2/24') 78 | set_ip(net, 'client','switch','192.168.100.3/24') 79 | 80 | def main(): 81 | topo = PySwitchTopo(args) 82 | net = Mininet(controller=None, topo=topo, link=TCLink, cleanup=True) 83 | setup_addressing(net) 84 | net.interact() 85 | 86 | if __name__ == '__main__': 87 | main() 88 | -------------------------------------------------------------------------------- /documentation/release_notes.rst: -------------------------------------------------------------------------------- 1 | Release notes 2 | ************* 3 | 4 | 1.0.0 5 | ----- 6 | Major revision, with fixes and updates to IPv6 classes and to generalize the ``Interface`` class so that any number of IP addresses may be assigned to it. ICMPv6 classes are basically complete for neighbor discovery and router solicitation and advertisement. These changes are **backward incompatible**. 7 | 8 | 9 | The headings below refer either to branches on Switchyard's github repo (v1 and v2) or tags (2017.01.1). 10 | 11 | 2017.01.4 12 | --------- 13 | More bugfixes. 14 | 15 | 2017.01.3 16 | --------- 17 | Minor bugfixes. 18 | 19 | 2017.01.2 20 | --------- 21 | 22 | Add the capability to pass arguments to a Switchyard program via ``-g`` option to ``swyard``. 23 | Switchyard parses and assembles ``*args`` and ``**kwargs`` to pass into the user code, being careful to only pass them if the code can accept them. 24 | 25 | 2017.01.1 26 | --------- 27 | 28 | Major revision; expansion of types of exercises supported (notably application-layer programs via socket emulation) and several non-backward compatible API changes. Simplified user code import (single import of switchyard.lib.userlib). Installation via standard setuptools, so easily installed via easy_install or pip. Major revision of documentation. Lots of new tests were written, bringing test coverage above 90%. Expansion of exercises is still in progress. 29 | 30 | Some key API changes to be aware of: 31 | 32 | * the Scenario class is renamed TestScenario. The PacketOutputEvent previously allowed Openflow 1.0-like wildcard strings to specify wildcards for matching packets; these strings are no longer supported. To specify wildcards, a tuple of (classname,attribute) must be used; refer to :ref:`test-scenario-creation`, above. 33 | * ``recv_packet`` *always* returns a timestamp now; it returns a 3-tuple (named tuple) of timestamp, input_port and packet. 34 | * The only import required by user code is switchyard.lib.userlib, although individual imports are still fine (just more verbose). 35 | * Instead of invoking ``srpy.py``, a ``swyard`` program is installed during the new install process. ``swyard`` has a few command-line changes compared with ``srpy.py``. In particular, the ``-s`` option has gone away; to run Switchyard with a test, just use the ``-t`` option with the scenario file as the argument. 36 | 37 | 38 | alphav2 39 | ------- 40 | 41 | Complete rewrite of v1. Moved to Python 3 and created packet parsing libraries, new libpcap interface library (pcapffi). Redesigned test scenario modules and an expanded of publicly available exercises. Used at Colgate twice and University of Wisconsin-Madison twice. Available on the ``v2`` branch on github. 42 | 43 | alphav1 44 | ------- 45 | 46 | First version, which used the POX packet parsing libraries and had a variety of limitations. Implemented in Python 2 and used at Colgate once. Available on the ``v1`` branch on github, but very much obsolete. 47 | -------------------------------------------------------------------------------- /docs/_sources/release_notes.rst.txt: -------------------------------------------------------------------------------- 1 | Release notes 2 | ************* 3 | 4 | 1.0.0 5 | ----- 6 | Major revision, with fixes and updates to IPv6 classes and to generalize the ``Interface`` class so that any number of IP addresses may be assigned to it. ICMPv6 classes are basically complete for neighbor discovery and router solicitation and advertisement. These changes are **backward incompatible**. 7 | 8 | 9 | The headings below refer either to branches on Switchyard's github repo (v1 and v2) or tags (2017.01.1). 10 | 11 | 2017.01.4 12 | --------- 13 | More bugfixes. 14 | 15 | 2017.01.3 16 | --------- 17 | Minor bugfixes. 18 | 19 | 2017.01.2 20 | --------- 21 | 22 | Add the capability to pass arguments to a Switchyard program via ``-g`` option to ``swyard``. 23 | Switchyard parses and assembles ``*args`` and ``**kwargs`` to pass into the user code, being careful to only pass them if the code can accept them. 24 | 25 | 2017.01.1 26 | --------- 27 | 28 | Major revision; expansion of types of exercises supported (notably application-layer programs via socket emulation) and several non-backward compatible API changes. Simplified user code import (single import of switchyard.lib.userlib). Installation via standard setuptools, so easily installed via easy_install or pip. Major revision of documentation. Lots of new tests were written, bringing test coverage above 90%. Expansion of exercises is still in progress. 29 | 30 | Some key API changes to be aware of: 31 | 32 | * the Scenario class is renamed TestScenario. The PacketOutputEvent previously allowed Openflow 1.0-like wildcard strings to specify wildcards for matching packets; these strings are no longer supported. To specify wildcards, a tuple of (classname,attribute) must be used; refer to :ref:`test-scenario-creation`, above. 33 | * ``recv_packet`` *always* returns a timestamp now; it returns a 3-tuple (named tuple) of timestamp, input_port and packet. 34 | * The only import required by user code is switchyard.lib.userlib, although individual imports are still fine (just more verbose). 35 | * Instead of invoking ``srpy.py``, a ``swyard`` program is installed during the new install process. ``swyard`` has a few command-line changes compared with ``srpy.py``. In particular, the ``-s`` option has gone away; to run Switchyard with a test, just use the ``-t`` option with the scenario file as the argument. 36 | 37 | 38 | alphav2 39 | ------- 40 | 41 | Complete rewrite of v1. Moved to Python 3 and created packet parsing libraries, new libpcap interface library (pcapffi). Redesigned test scenario modules and an expanded of publicly available exercises. Used at Colgate twice and University of Wisconsin-Madison twice. Available on the ``v2`` branch on github. 42 | 43 | alphav1 44 | ------- 45 | 46 | First version, which used the POX packet parsing libraries and had a variety of limitations. Implemented in Python 2 and used at Colgate once. Available on the ``v1`` branch on github, but very much obsolete. 47 | -------------------------------------------------------------------------------- /docs/_sources/installation.rst.txt: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installing Switchyard 4 | ********************* 5 | 6 | Switchyard has been tested and developed on the following operating systems: 7 | 8 | * macOS 10.10 and later 9 | * Ubuntu LTS releases from 14.04 and later 10 | * Fedora 21 11 | 12 | Note that these are all Unix-based systems. Switchyard may be enhanced in the future to support Windows-based systems. Ubuntu (current LTS) and macOS receive the most testing of Unix-based operating systems. 13 | 14 | --- 15 | 16 | The steps for getting Switchyard up and running are as follows: 17 | 18 | 0. Install Python 3.4 or later, if you don't already have it. 19 | 1. Install any necessary libraries for your operating system. 20 | 2. Create an Python "virtual environment" for installing Python modules (or install the modules to your system Python) 21 | 3. Install Switchyard. 22 | 23 | For step 0, you're on your own. Go to https://www.python.org/downloads/, or install packages via your OS'es package system, or use homebrew if you're on a Mac. Have fun. 24 | 25 | The specific libraries necessary for different OSes (step 1) are described below, but steps 2 and 3 are the same for all operating systems and are covered next. 26 | 27 | The recommended install procedure is to create a Python virtual environment for installing Switchyard and other required Python modules. One way to create a new virtual environment is to execute the following at a command line (in the folder in which you want to create the virtual environment):: 28 | 29 | $ python3 -m venv syenv 30 | 31 | This command will create a new virtual environment called ``syenv``. Once that's done, you can "activate" that environment and install Switchyard as follows:: 32 | 33 | $ source ./syenv/bin/activate 34 | (syenv)$ python3 -m pip install switchyard 35 | 36 | That's it. Once you've done that, the ``swyard`` program should be on your ``PATH`` (you can check by typing ``which swyard``). If you no longer want to use the Python virtual environment you've created, you can just type ``deactivate``. 37 | 38 | Operating system-specific instructions 39 | ====================================== 40 | 41 | MacOS X 42 | ------- 43 | 44 | The easiest way to get Switchyard running in macOS is to install homebrew. You can use ``brew`` to install Python 3. You should also ``brew`` to install the ``libpcap`` package. That should be all that is necessary. 45 | 46 | Ubuntu 47 | ------ 48 | 49 | For Ubuntu systems, you'll need to use ``apt-get`` or something similar to install the following packages:: 50 | 51 | libffi-dev libpcap-dev python3-dev python3-pip python3-venv 52 | 53 | Fedora/RedHat 54 | ------------- 55 | 56 | For Fedora and RedHat-based systems, you'll need to use ``yum`` or something similar to install a similar set of packages as with Ubuntu (but with the right name changes for the way packages are identified on Fedora):: 57 | 58 | libffi-devel libpcap-devel python3-devel python3-pip python3-virtualenv 59 | 60 | -------------------------------------------------------------------------------- /documentation/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installing Switchyard 4 | ********************* 5 | 6 | Switchyard has been tested and developed on the following operating systems: 7 | 8 | * macOS 10.10 and later 9 | * Ubuntu LTS releases from 14.04 and later 10 | * Fedora 21 11 | 12 | Note that these are all Unix-based systems. Switchyard may be enhanced in the future to support Windows-based systems. Ubuntu (current LTS) and macOS receive the most testing of Unix-based operating systems. 13 | 14 | --- 15 | 16 | The steps for getting Switchyard up and running are as follows: 17 | 18 | 0. Install Python 3.4 or later, if you don't already have it. 19 | 1. Install any necessary libraries for your operating system. 20 | 2. Create an Python "virtual environment" for installing Python modules (or install the modules to your system Python) 21 | 3. Install Switchyard. 22 | 23 | For step 0, you're on your own. Go to https://www.python.org/downloads/, or install packages via your OS'es package system, or use homebrew if you're on a Mac. Have fun. 24 | 25 | The specific libraries necessary for different OSes (step 1) are described below, but steps 2 and 3 are the same for all operating systems and are covered next. 26 | 27 | The recommended install procedure is to create a Python virtual environment for installing Switchyard and other required Python modules. One way to create a new virtual environment is to execute the following at a command line (in the folder in which you want to create the virtual environment):: 28 | 29 | $ python3 -m venv syenv 30 | 31 | This command will create a new virtual environment called ``syenv``. Once that's done, you can "activate" that environment and install Switchyard as follows:: 32 | 33 | $ source ./syenv/bin/activate 34 | (syenv)$ python3 -m pip install switchyard 35 | 36 | That's it. Once you've done that, the ``swyard`` program should be on your ``PATH`` (you can check by typing ``which swyard``). If you no longer want to use the Python virtual environment you've created, you can just type ``deactivate``. 37 | 38 | Operating system-specific instructions 39 | ====================================== 40 | 41 | MacOS X 42 | ------- 43 | 44 | The easiest way to get Switchyard running in macOS is to install homebrew. You can use ``brew`` to install Python 3. You should also ``brew`` to install the ``libpcap`` package. That should be all that is necessary. 45 | 46 | Ubuntu 47 | ------ 48 | 49 | For Ubuntu systems, you'll need to use ``apt-get`` or something similar to install the following packages:: 50 | 51 | libffi-dev libpcap-dev python3-dev python3-pip python3-venv 52 | 53 | Fedora/RedHat 54 | ------------- 55 | 56 | For Fedora and RedHat-based systems, you'll need to use ``yum`` or something similar to install a similar set of packages as with Ubuntu (but with the right name changes for the way packages are identified on Fedora):: 57 | 58 | libffi-devel libpcap-devel python3-devel python3-pip python3-virtualenv 59 | 60 | -------------------------------------------------------------------------------- /examples/exercises/firewall/start_mininet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | from mininet.topo import Topo 6 | from mininet.net import Mininet 7 | from mininet.log import lg 8 | from mininet.node import CPULimitedHost 9 | from mininet.link import TCLink 10 | from mininet.util import irange, custom, quietRun, dumpNetConnections 11 | from mininet.cli import CLI 12 | 13 | from time import sleep, time 14 | from subprocess import Popen, PIPE 15 | import subprocess 16 | import argparse 17 | import os 18 | 19 | parser = argparse.ArgumentParser(description="Mininet portion of pyrouter") 20 | # no arguments needed as yet :-) 21 | args = parser.parse_args() 22 | lg.setLogLevel('info') 23 | 24 | class PyRouterTopo(Topo): 25 | 26 | def __init__(self, args): 27 | # Add default members to class. 28 | super(PyRouterTopo, self).__init__() 29 | 30 | # Host and link configuration 31 | # 32 | # external----firewall----internal 33 | # 34 | # external refers to the internet outside a given 35 | # enterprise's network. internal refers to the 36 | # enterprise's network. 37 | 38 | self.addHost('external') 39 | self.addHost('internal') 40 | self.addHost('firewall') 41 | 42 | for node in ['internal','external']: 43 | self.addLink(node, 'firewall', bw=1000, delay="10ms") 44 | 45 | def set_ip_pair(net, node1, node2, ip1, ip2): 46 | node1 = net.get(node1) 47 | ilist = node1.connectionsTo(net.get(node2)) # returns list of tuples 48 | intf = ilist[0] 49 | intf[0].setIP(ip1) 50 | intf[1].setIP(ip2) 51 | 52 | def set_ip(net, node, ifname, addr): 53 | node_object = net.get(node) 54 | intf = node_object.intf(ifname) 55 | intf.setIP(addr) 56 | 57 | def reset_macs(net, node, macbase): 58 | ifnum = 1 59 | node_object = net.get(node) 60 | for intf in node_object.intfList(): 61 | if node not in str(intf): 62 | continue # don't set lo or other interfaces 63 | node_object.setMAC(macbase.format(ifnum), intf) 64 | ifnum += 1 65 | 66 | for intf in node_object.intfList(): 67 | print node,intf,node_object.MAC(intf) 68 | 69 | def set_def_route(net, fromnode, gw): 70 | node_object = net.get(fromnode) 71 | node_object.cmdPrint("route add default gw {}".format(gw)) 72 | 73 | def setup_addressing(net): 74 | reset_macs(net, 'internal', '00:00:00:00:01:{:02x}') 75 | reset_macs(net, 'external', '00:00:00:00:10:{:02x}') 76 | reset_macs(net, 'firewall', '00:00:00:00:0b:{:02x}') 77 | 78 | set_ip(net,'internal','internal-eth0','192.168.0.1/24') 79 | set_ip(net,'external','external-eth0','192.168.0.2/24') 80 | 81 | def stop_nodegrams(net): 82 | for nname in ['external','internal','firewall']: 83 | n = net.get(nname) 84 | n.cmd("killall python3") 85 | 86 | def main(): 87 | topo = PyRouterTopo(args) 88 | net = Mininet(topo=topo, link=TCLink, cleanup=True, autoSetMacs=True, controller=None) 89 | setup_addressing(net) 90 | net.staticArp() 91 | net.interact() 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /examples/exercises/firewall/impairmenttest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from copy import deepcopy 4 | import random 5 | 6 | from switchyard.lib.userlib import * 7 | 8 | firewall_rules = ''' 9 | # drop everything from an internal subnet which shouldn't be allowed 10 | # to communicate with rest of internet 11 | # rule 1 12 | deny ip src 192.168.42.0/24 dst any 13 | # rule 2 14 | deny ip src any dst 192.168.42.0/24 15 | 16 | # allow traffic to/from an internal web server that should 17 | # be accessible to external hosts 18 | # rule 3 19 | permit tcp src 192.168.13.13 srcport 80 dst any dstport any 20 | # rule 4 21 | permit tcp src any srcport any dst 192.168.13.13 dstport 80 22 | 23 | # allow DNS (udp port 53) traffic in/out of network 24 | # rule 5 25 | permit udp src 192.168.0.0/16 srcport any dst any dstport 53 26 | # rule 6 27 | permit udp src any srcport 53 dst 192.168.0.0/16 dstport any 28 | 29 | # allow internal hosts access to web (tcp ports 80 and 443) 30 | # rate limit http traffic to 100 kB/s (12500 bytes/sec), but 31 | # don't rate limit any encrypted HTTP traffic. 32 | # rule 7 33 | permit tcp src 192.168.0.0/16 srcport any dst any dstport 80 ratelimit 12500 34 | # rule 8 35 | permit tcp src any srcport any dst 172.16.42.0/24 dstport 80 ratelimit 12500 36 | # rule 9 37 | permit tcp src 192.168.0.0/16 srcport any dst any dstport 443 38 | # rule 10 39 | permit tcp src any srcport any dst 172.16.42.0/24 dstport 443 40 | 41 | # permit, but impair certain traffic flows 42 | # rule 11 43 | permit tcp src 192.168.0.0/24 srcport any dst any dstport 8000 impair 44 | 45 | # permit, but rate limit icmp to 100 bytes/sec 46 | # rule 12 47 | permit icmp src any dst any ratelimit 100 48 | 49 | # block everything else 50 | # rule 13 51 | deny ip src any dst any 52 | ''' 53 | 54 | def firewall_tests(): 55 | s = TestScenario("Firewall tests") 56 | s.add_file('firewall_rules.txt', firewall_rules) 57 | 58 | # two ethernet ports; no IP addresses assigned to 59 | # them. eth0 is internal network-facing, and eth1 60 | # is external network-facing. 61 | s.add_interface('eth0', '00:00:00:00:0b:01') 62 | s.add_interface('eth1', '00:00:00:00:0b:02') 63 | 64 | t = TCP() 65 | t.ACK = 1 66 | t.ack = random.randrange(0,2**32) 67 | t.seq = random.randrange(0,2**32) 68 | t.src = random.randrange(2**12,2**16) 69 | t.dst = 8000 70 | ip = IPv4() 71 | ip.src = '192.168.0.13' 72 | ip.dst = IPv4Address(random.randrange(2**16, 2**32)) 73 | ip.protocol = IPProtocol.TCP 74 | pkt = Ethernet() + ip + t + "This is some TCP data!".encode() 75 | # fill in any other packet headers or data to the constructed packet 76 | s.expect(PacketInputEvent('eth0',pkt), 77 | 'Packet that should be impaired arrives on eth0') 78 | 79 | # Modify the packet in the same way that you expect the firewall 80 | # to modify the packet. 81 | pkt = deepcopy(pkt) # make a full copy of the packet before modifying 82 | 83 | s.expect(PacketOutputEvent('eth1',pkt), 84 | 'Test description for a packet departure --- what should happen?') 85 | 86 | return s 87 | 88 | scenario = firewall_tests() 89 | -------------------------------------------------------------------------------- /tests/test_ofswitch_ext.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | OF switch tests for use with an external controller. 5 | ''' 6 | 7 | import sys 8 | from switchyard.lib.address import * 9 | from switchyard.lib.packet import * 10 | from switchyard.lib.testing import * 11 | from switchyard.lib.openflow import * 12 | 13 | 14 | def mk_pkt(hwsrc, hwdst, ipsrc, ipdst, reply=False): 15 | ether = Ethernet() 16 | ether.src = EthAddr(hwsrc) 17 | ether.dst = EthAddr(hwdst) 18 | ether.ethertype = EtherType.IP 19 | 20 | ippkt = IPv4() 21 | ippkt.src = ip_address(ipsrc) 22 | ippkt.dst = ip_address(ipdst) 23 | ippkt.protocol = IPProtocol.ICMP 24 | ippkt.ttl = 32 25 | 26 | icmppkt = ICMP() 27 | if reply: 28 | icmppkt.icmptype = ICMPType.EchoReply 29 | else: 30 | icmppkt.icmptype = ICMPType.EchoRequest 31 | return ether + ippkt + icmppkt 32 | 33 | 34 | def ofswitch_tests(): 35 | s = TestScenario("Openflow Switch Tests") 36 | s.add_interface('eth0', '10:00:00:00:00:01') 37 | s.add_interface('eth1', '10:00:00:00:00:02') 38 | s.add_interface('eth2', '10:00:00:00:00:03') 39 | 40 | # test case 1: a frame with broadcast destination should get sent out 41 | # all ports except ingress 42 | testpkt = mk_pkt( 43 | "30:00:00:00:00:02", "ff:ff:ff:ff:ff:ff", "172.16.42.2", "255.255.255.255") 44 | s.expect(PacketInputEvent("eth1", testpkt, display=Ethernet), 45 | "An Ethernet frame with a broadcast destination address should arrive on eth1") 46 | s.expect(PacketInputTimeoutEvent(timeout=5), description="Wait for events to complete") 47 | s.expect(PacketOutputEvent("eth2", testpkt, "eth0", testpkt, display=Ethernet), 48 | "The Ethernet frame with a broadcast destination address should be forwarded out ports eth0 and eth2") 49 | # test case 2: frame with dest 30:...:02 should be sent out eth 1 (from where bcast frame arrived) 50 | testpkt2 = mk_pkt( 51 | "30:00:00:00:00:03", "30:00:00:00:00:02", "10.0.42.200", "172.16.42.2") 52 | s.expect(PacketInputEvent("eth2", testpkt2, display=Ethernet), 53 | "An Ethernet frame with a destination address 172.16.42.2 should arrive on eth2") 54 | s.expect(PacketInputTimeoutEvent(timeout=5), description="Wait for events to complete") 55 | s.expect(PacketOutputEvent("eth1", testpkt2, display=Ethernet), 56 | "An Ethernet frame with a destination address 172.16.42.2 should be forwarded out port eth1") 57 | s.expect(PacketInputTimeoutEvent(timeout=5), description="Wait for events to complete") 58 | 59 | # test case 3: dest port for 30::03 should have been learned from previous exchange 60 | testpkt3 = mk_pkt( 61 | "30:00:00:00:00:01", "30:00:00:00:00:03", "172.16.42.2", "10.1.13.13") 62 | s.expect(PacketInputEvent("eth0", testpkt3, display=Ethernet), 63 | "An Ethernet frame with a destination address 10.1.13.13 should arrive on eth0") 64 | s.expect(PacketInputTimeoutEvent(timeout=5), description="Wait for events to complete") 65 | s.expect(PacketOutputEvent("eth2", testpkt3, display=Ethernet), 66 | "An Ethernet frame with a destination address 10.1.13.13 should be forwarded out port eth2") 67 | 68 | return s 69 | 70 | 71 | scenario = ofswitch_tests() 72 | -------------------------------------------------------------------------------- /tests/test_ripv2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from switchyard.lib.packet import * 4 | from switchyard.pcapffi import PcapDumper 5 | 6 | class RIPv2PacketTests(unittest.TestCase): 7 | def testRequest(self): 8 | p = Ethernet() + IPv4() + UDP() + RIPv2() 9 | p[0].src = '00:11:22:33:44:55' 10 | p[0].dst = '55:44:33:22:11:00' 11 | p[1].protocol = IPProtocol.UDP 12 | p[1].src = '192.168.100.42' 13 | p[1].dst = '192.168.100.255' 14 | p[2].src = 5000 15 | p[2].dst = 520 16 | xraw = p.to_bytes() 17 | pkt = Packet(raw=xraw) 18 | pkt[-1] = RIPv2(pkt[-1]) 19 | self.assertEqual(pkt, p) 20 | self.assertEqual(pkt[-1].size(), 4) 21 | 22 | self.assertIn("RIPv2 Request", str(pkt[-1])) 23 | 24 | def testReply(self): 25 | p = Ethernet() + IPv4() + UDP() + RIPv2() 26 | p[0].src = '00:11:22:33:44:55' 27 | p[0].dst = '55:44:33:22:11:00' 28 | p[1].protocol = IPProtocol.UDP 29 | p[1].src = '192.168.100.42' 30 | p[1].dst = '192.168.100.255' 31 | p[2].src = 5000 32 | p[2].dst = 520 33 | p[3].command = RIPCommand.Reply 34 | p[3].append(RIPRouteEntry('192.168.200.0','255.255.255.0','192.168.200.254',4)) 35 | p[3].append(RIPRouteEntry('192.168.100.0','255.255.252.0','192.168.100.254',3)) 36 | xraw = p.to_bytes() 37 | pkt = Packet(raw=xraw) 38 | pkt[-1] = RIPv2(pkt[-1]) 39 | self.assertEqual(pkt, p) 40 | s = str(pkt) 41 | self.assertIn('192.168.200.0/24', s) 42 | self.assertIn('192.168.100.0/22', s) 43 | self.assertEqual(pkt[-1][0].address, IPv4Address('192.168.200.0')) 44 | self.assertEqual(pkt[-1][0].netmask, IPv4Address('255.255.255.0')) 45 | self.assertEqual(pkt.size(), 44) 46 | self.assertEqual(pkt[3].size(), 44) 47 | 48 | pkt[-1][-1] = RIPRouteEntry('192.168.0.0','255.255.0.0','192.168.42.5',15) 49 | s = str(pkt) 50 | self.assertIn('192.168.0.0/16', s) 51 | self.assertNotIn('192.168.100.0/22', s) 52 | with self.assertRaises(ValueError): 53 | pkt[-1].append(1) 54 | with self.assertRaises(ValueError): 55 | pkt[-1][0] = 0 56 | with self.assertRaises(IndexError): 57 | pkt[-1][2] = RIPRouteEntry() 58 | with self.assertRaises(IndexError): 59 | x = pkt[-1][2] 60 | 61 | with self.assertRaises(TypeError): 62 | x = pkt[-1]["a"] 63 | lastentry = pkt[-1][-1] 64 | self.assertEqual(lastentry.address, IPv4Address("192.168.0.0")) 65 | self.assertEqual(lastentry.netmask, IPv4Address("255.255.0.0")) 66 | self.assertEqual(lastentry.nexthop, IPv4Address("192.168.42.5")) 67 | self.assertEqual(lastentry.metric, RIP_INFINITY) 68 | 69 | re = RIPRouteEntry('192.168.100.0','255.255.252.0','192.168.100.254',3) 70 | re2 = RIPRouteEntry.from_bytes(re.to_bytes()) 71 | self.assertEqual(re, re2) 72 | with self.assertRaises(Exception): 73 | re2.from_bytes(re.to_bytes()[:-1]) 74 | 75 | r1 = pkt[-1] 76 | b = r1.to_bytes() 77 | r2 = RIPv2() 78 | with self.assertRaises(Exception): 79 | r2.from_bytes(b[:3]) 80 | 81 | with self.assertLogs() as cm: 82 | b += b'\x11\x22\x33' 83 | r2.from_bytes(b) 84 | self.assertIn("payload isn't of expected size", cm.output[0]) 85 | 86 | with self.assertLogs() as cm: 87 | r2.from_bytes(b[:-9]) 88 | self.assertIn("payload isn't of expected size", cm.output[0]) 89 | 90 | self.assertIsNone(r1.next_header_class()) 91 | 92 | self.assertIn("RIPv2 Reply (2 routes", str(r1)) 93 | 94 | 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | 99 | -------------------------------------------------------------------------------- /docs/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Search — Switchyard 1.0.1 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |

Search

42 | 43 | 51 | 52 | 53 |

54 | Searching for multiple words only shows matches that contain 55 | all words. 56 |

57 | 58 | 59 |
60 | 61 | 62 | 63 |
64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 | 72 |
73 |
74 |
75 |
76 | 80 |
81 |
82 | 95 | 99 | 100 | -------------------------------------------------------------------------------- /examples/exercises/router/start_mininet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | from mininet.topo import Topo 6 | from mininet.net import Mininet 7 | from mininet.log import lg 8 | from mininet.node import CPULimitedHost 9 | from mininet.link import TCLink 10 | from mininet.util import irange, custom, quietRun, dumpNetConnections 11 | from mininet.cli import CLI 12 | 13 | from time import sleep, time 14 | from subprocess import Popen, PIPE 15 | import subprocess 16 | import argparse 17 | import os 18 | 19 | parser = argparse.ArgumentParser(description="Mininet portion of pyrouter") 20 | # no arguments needed as yet :-) 21 | args = parser.parse_args() 22 | lg.setLogLevel('info') 23 | 24 | class PyRouterTopo(Topo): 25 | 26 | def __init__(self, args): 27 | # Add default members to class. 28 | super(PyRouterTopo, self).__init__() 29 | 30 | # Host and link configuration 31 | # 32 | # 33 | # server1 34 | # \ 35 | # router----client 36 | # / 37 | # server2 38 | # 39 | 40 | nodeconfig = {'cpu':-1} 41 | self.addHost('server1', **nodeconfig) 42 | self.addHost('server2', **nodeconfig) 43 | self.addHost('router', **nodeconfig) 44 | self.addHost('client', **nodeconfig) 45 | 46 | linkconfig = { 47 | 'bw': 10, 48 | 'delay': 0.010, 49 | 'loss': 0.0 50 | } 51 | 52 | for node in ['server1','server2','client']: 53 | self.addLink(node, 'router', **linkconfig) 54 | 55 | def set_ip_pair(net, node1, node2, ip1, ip2): 56 | node1 = net.get(node1) 57 | ilist = node1.connectionsTo(net.get(node2)) # returns list of tuples 58 | intf = ilist[0] 59 | intf[0].setIP(ip1) 60 | intf[1].setIP(ip2) 61 | 62 | def reset_macs(net, node, macbase): 63 | ifnum = 1 64 | node_object = net.get(node) 65 | for intf in node_object.intfList(): 66 | node_object.setMAC(macbase.format(ifnum), intf) 67 | ifnum += 1 68 | 69 | for intf in node_object.intfList(): 70 | print node,intf,node_object.MAC(intf) 71 | 72 | def set_route(net, fromnode, prefix, gw): 73 | node_object = net.get(fromnode) 74 | node_object.cmdPrint("route add -net {} gw {}".format(prefix, gw)) 75 | 76 | def setup_addressing(net): 77 | reset_macs(net, 'server1', '10:00:00:00:00:{:02x}') 78 | reset_macs(net, 'server2', '20:00:00:00:00:{:02x}') 79 | reset_macs(net, 'client', '30:00:00:00:00:{:02x}') 80 | reset_macs(net, 'router', '40:00:00:00:00:{:02x}') 81 | set_ip_pair(net, 'server1','router','192.168.100.1/30','192.168.100.2/30') 82 | set_ip_pair(net, 'server2','router','192.168.200.1/30','192.168.200.2/30') 83 | set_ip_pair(net, 'client','router','10.1.1.1/30','10.1.1.2/30') 84 | set_route(net, 'server1', '10.1.0.0/16', '192.168.100.2') 85 | set_route(net, 'server1', '192.168.200.0/24', '192.168.100.2') 86 | set_route(net, 'server2', '10.1.0.0/16', '192.168.200.2') 87 | set_route(net, 'server2', '192.168.100.0/24', '192.168.200.2') 88 | set_route(net, 'client', '192.168.100.0/24', '10.1.1.2') 89 | set_route(net, 'client', '192.168.200.0/24', '10.1.1.2') 90 | set_route(net, 'client', '172.16.0.0/16', '10.1.1.2') 91 | 92 | forwarding_table = open('forwarding_table.txt', 'w') 93 | table = '''192.168.100.0 255.255.255.0 192.168.100.1 router-eth0 94 | 192.168.200.0 255.255.255.0 192.168.200.1 router-eth1 95 | 10.1.0.0 255.255.0.0 10.1.1.1 router-eth2 96 | ''' 97 | forwarding_table.write(table) 98 | forwarding_table.close() 99 | 100 | 101 | def main(): 102 | topo = PyRouterTopo(args) 103 | net = Mininet(topo=topo, link=TCLink, cleanup=True, controller=None) 104 | setup_addressing(net) 105 | net.interact() 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /tests/test_pcapffi.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | from unittest.mock import Mock 5 | 6 | from switchyard.lib.packet import * 7 | import switchyard.pcapffi as pf 8 | 9 | class PcapFfiTests(unittest.TestCase): 10 | def testWriteRead1(self): 11 | dump = pf.PcapDumper("testXX.pcap") 12 | pkt = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\x1c\x00\x00\x00\x00\x00\x01\xba\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\xf7\xff\x00\x00\x00\x00' 13 | dump.write_packet(pkt) 14 | with self.assertRaises(pf.PcapException): 15 | dump.write_packet("hello, world") 16 | dump.close() 17 | 18 | reader = pf.PcapReader("testXX.pcap") 19 | reader.set_filter("icmp") 20 | pkts = [] 21 | while True: 22 | p = reader.recv_packet() 23 | if p is None: 24 | break 25 | pkts.append(p) 26 | reader.close() 27 | self.assertEqual(len(pkts), 1) 28 | self.assertEqual(pkts[0].capture_length, len(pkt)) 29 | self.assertEqual(pkts[0].length, len(pkt)) 30 | self.assertEqual(pkts[0].raw, pkt) 31 | os.unlink("testXX.pcap") 32 | 33 | def testWriteRead2(self): 34 | dump = pf.PcapDumper("testXX.pcap") 35 | pkt = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00E\x00\x00\x1c\x00\x00\x00\x00\x00\x01\xba\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\xf7\xff\x00\x00\x00\x00' 36 | dump.write_packet(pkt) 37 | with self.assertRaises(pf.PcapException): 38 | dump.write_packet("hello, world") 39 | dump.close() 40 | 41 | reader = pf.PcapReader("testXX.pcap") 42 | reader.set_filter("tcp") 43 | pkts = [] 44 | while True: 45 | p = reader.recv_packet() 46 | if p is None: 47 | break 48 | pkts.append(p) 49 | reader.close() 50 | self.assertEqual(len(pkts), 0) 51 | os.unlink("testXX.pcap") 52 | 53 | def testAnotherInstance(self): 54 | with self.assertRaises(Exception): 55 | pf._PcapFfi() 56 | 57 | with self.assertRaises(Exception): 58 | pf._PcapFfi._instance.discoverdevs() 59 | 60 | def testCreate(self): 61 | devs = pf.pcap_devices() 62 | xname = devs[0].name 63 | px = pf.PcapLiveDevice.create(xname) 64 | px.snaplen = 80 65 | px.set_promiscuous(True) 66 | px.set_timeout(0) 67 | px.set_immediate_mode(True) 68 | px.set_buffer_size(4096) 69 | try: 70 | px.set_tstamp_type(PcapTstampType.Host) 71 | except: 72 | pass 73 | self.assertEqual(px.tstamp_precision, pf.PcapTstampPrecision.Micro) 74 | xlist = px.list_tstamp_types() 75 | self.assertIsInstance(xlist, list) 76 | try: 77 | px.tstamp_precision = PcapTstampPrecision.Nano 78 | self.assertEqual(px.tstamp_precision, pf.PcapTstampPrecision.Nano) 79 | except: 80 | self.assertEqual(px.tstamp_precision, pf.PcapTstampPrecision.Micro) 81 | with self.assertRaises(pf.PcapException): 82 | px.blocking 83 | px.blocking = True 84 | with self.assertRaises(pf.PcapException): 85 | px.blocking 86 | with self.assertRaises(pf.PcapException): 87 | x = px.dlt # exc because not activated 88 | self.assertEqual(px.fd, -1) 89 | with self.assertRaises(pf.PcapException): 90 | px.send_packet("hello, world") # not bytes 91 | self.assertIsNone(px.recv_packet(0)) 92 | self.assertIsNone(px.recv_packet_or_none()) 93 | with self.assertRaises(pf.PcapException): 94 | px.set_direction(pf.PcapDirection.InOut) # not active 95 | with self.assertRaises(pf.PcapException): 96 | px.set_filter("icmp") # not active 97 | px.close() 98 | 99 | if __name__ == '__main__': 100 | unittest.main(verbosity=1) 101 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ******** 3 | 4 | This folder contains a few different examples of Switchyard user code, as described below. Sample exercises intended for classroom use can be found in the ``exercises`` folder. For further details and explanation of various API calls in the examples below, please refer to the documentation. For additional instructor materials (including exercises, full tests, etc.), please email jsommers@colgate.edu. 5 | 6 | Basic send/receive examples 7 | --------------------------- 8 | 9 | There are a few simple examples to illustrate sending and receiving packets using Switchyard. 10 | 11 | ``sendpkt.py`` 12 | This program is designed to send one packet (an ICMP echo request) out each available interface. It should be invoked as a Switchyard program (not as a regular Python program). For example: ``swyard sendpkt.py``. You will likely need to run this program as root in order to successfully send packets. 13 | 14 | ``sniff.py`` 15 | This program is a simple "packet sniffer", which just prints out a string representation of each packet as it is received. It should be invoked as a Switchyard program, e.g., ``swyard sniff.py``. It may need to be run as root in order to successfully capture and print packets. 16 | 17 | ``sydump.py`` 18 | This program *uses* the Switchyard libraries, but is just executed as a regular Python program. It captures packets from one interface, prints out each packet, and also stores the packet in a file named ``sydump.pcap``. 19 | 20 | This program can just be invoked as ``python3 sydump.py``, assuming Switchyard and its dependencies have been installed. 21 | 22 | ``readpkt.py`` 23 | This program also just *uses* the Switchyard libraries but is just executed as a regular Python program. I reads packet data stored in a libpcap file and prints a string representation of each packet to the console. 24 | 25 | This program can just be invoked as ``python3 readpkt.py``, assuming Switchyard and its dependencies have been installed. 26 | 27 | Network hub example 28 | ------------------- 29 | 30 | The network hub example is discussed quite extensively in the documentation. There are two files here related to that example: ``hubtests.py`` and ``myhub.py``. It can be executed as ``swyard -t hubtests.py myhub.py`` 31 | 32 | Application layer code + stack example 33 | -------------------------------------- 34 | 35 | Switchyard contains a *socket emulation capability* which can be used to run a (mostly) standard UDP-based Python socket program and have that program use a Switchyard-based networking stack. There are four example files related to this capability: 36 | 37 | ``clientapp_udpstackex.py`` 38 | A client socket-based program that sends a UDP message to 127.0.0.1:10000 and waits for one response back from a server. 39 | 40 | ``server_udpstackex.py`` 41 | A server socket-based program that binds to UDP port 10000 and echos back whatever gets sent to it. 42 | 43 | ``udpstack_tests.py`` 44 | A test file that waits for a message emitted from a client program and mimics a server's response back to the client. 45 | 46 | ``udpstack.py`` 47 | A Switchyard program that implements the very basics of a UDP networking stack. It presently assumes that only the localhost interface is used (thus ARP and a forwarding decision are not required). To run this program in *real* mode currently requires use of macos (Linux and other OSes are not yet supported). 48 | 49 | To run this example in test mode, you can use the following command line: 50 | ``swyard -t udpstack_tests.py -a clientapp_udpstackex.py udpstack.py`` 51 | 52 | To run this example in real mode, you might want to start the server first, but you don't actually need to:: 53 | 54 | $ python3 server_udpstackex.py 55 | 56 | The Switchyard component(s) then can be executed as:``swyard -i lo0 -a clientapp_udpstackex.py udpstack.py``. Note that if the server isn't started, you should see an ICMP destination unreachable (port unreachable) error message returned to the stack. 57 | 58 | Finally, note that to execute the client and server *without* any Switchyard involvement requires a single line edit in the client file to import the Python socket library instead of Switchyard's socket emulation library. 59 | -------------------------------------------------------------------------------- /docs/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Python Module Index — Switchyard 1.0.1 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 32 | 33 |
34 |
35 |
36 |
37 | 38 | 39 |

Python Module Index

40 | 41 |
42 | s 43 |
44 | 45 | 46 | 47 | 49 | 50 | 52 | 55 | 56 | 57 | 60 |
 
48 | s
53 | switchyard 54 |
    58 | switchyard.lib.userlib 59 |
61 | 62 | 63 |
64 |
65 |
66 |
67 | 81 |
82 |
83 | 96 | 100 | 101 | -------------------------------------------------------------------------------- /documentation/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction and Overview 2 | ************************* 3 | 4 | Switchyard is a framework for creating, testing, and experimenting with software implementations of networked systems such as Ethernet switches, IP routers, firewalls and middleboxes, and end-host protocol stacks. Switchyard can be used for system-building projects targeting layers of the network protocol stack from layer 2 (link layer) and above. It is intended primarily for educational use and has purpose-built testing and debugging features. Although its design favors understandability over speed, it can work quite nicely as a prototyping environment for new kinds of networked devices. 5 | 6 | The Switchyard framework is implemented in Python and consists of two components: a program (``swyard``) which creates a runtime environment for the code that implements some networked system or device, and a collection of library modules that can be used for a variety of tasks such as packet creation and parsing. The networked system code is implemented in one or more Python files (which you write!) and that use the Switchyard libraries and conform to certain conventions. The ``swyard`` runtime environment creator and orchestrator seamlessly handles running your code either in a test setting where no actual network traffic is generated or in a real or "live" setting in which your code can interact with other networked systems. 7 | 8 | The Switchyard runtime environment (depicted below) provides a given networked system with 1 or more *interfaces* or *ports*. A port may represent a wired connection to another device, or may represent a wireless interface, or may represent a *loopback* [#loopback]_ interface. In any case, it is through these ports that packets are sent and received. Each port has, at minimum, a name (e.g., ``en0``) and an Ethernet address. A port may also have an IPv4 address and subnet mask associated with it. 9 | 10 | 11 | .. figure:: srpyarch.* 12 | :align: center 13 | :figwidth: 80% 14 | 15 | 16 | The typical goal of a Switchyard-based program is to receive a packet on one port, possibly modify it, then either forward it out one or more ports or to drop the packet. The rest of this documentation is organized around how to perform these tasks in various settings. In particular: 17 | 18 | * The next section, :ref:`writing a Switchyard program `, describes how to develop a basic Switchyard program, including what APIs are available for parsing and constructing packets and sending/receiving packets on network interfaces. 19 | * Following that, the next section, :ref:`running in the test environment `, provides details on running a Switchyard program in the test environment. The section after that gives details on :ref:`how to write test scenarios`. 20 | * The next section describes :ref:`running Switchyard in a live environment `, such as on a standard Linux host or within the Mininet emulation environment or some other kind of virtual environment. 21 | * :ref:`Advanced API topics ` are addressed next, such as creating new packet header types, and implementing network protocol stacks that can interoperate with a Python socket-based program. 22 | * An :ref:`installation guide ` appears next. 23 | * Finally, you can find an :ref:`apiref` at the end of this documentation along with and an index. 24 | 25 | **A note to the pedantic**: In this documentation we use the term *packet* in a generic sense to refer to what may more traditionally be a link layer *frame*, a network layer *packet*, a transport layer *segment*, or an application layer *message*. Where appropriate, we use the appropriate specific term, but often resort to using *packet* in a more general sense. 26 | 27 | **And one more (genuinely important) note**: Switchyard is Python 3-only! You'll get an error (or maybe even more than one error!) if you try to use Switchyard with Python 2. Python 3.4 is required, at minimum. An installation guide (see :ref:`install`) is provided in this documentation to help with getting any necessary libraries installed on your platform to make Switchyard work right. 28 | 29 | 30 | .. rubric:: Footnotes 31 | 32 | .. [#loopback] The loopback interface is a *virtual* interface that connects a host to itself. It is typically used to facilitate network communication among processes on the same host. 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/_sources/intro.rst.txt: -------------------------------------------------------------------------------- 1 | Introduction and Overview 2 | ************************* 3 | 4 | Switchyard is a framework for creating, testing, and experimenting with software implementations of networked systems such as Ethernet switches, IP routers, firewalls and middleboxes, and end-host protocol stacks. Switchyard can be used for system-building projects targeting layers of the network protocol stack from layer 2 (link layer) and above. It is intended primarily for educational use and has purpose-built testing and debugging features. Although its design favors understandability over speed, it can work quite nicely as a prototyping environment for new kinds of networked devices. 5 | 6 | The Switchyard framework is implemented in Python and consists of two components: a program (``swyard``) which creates a runtime environment for the code that implements some networked system or device, and a collection of library modules that can be used for a variety of tasks such as packet creation and parsing. The networked system code is implemented in one or more Python files (which you write!) and that use the Switchyard libraries and conform to certain conventions. The ``swyard`` runtime environment creator and orchestrator seamlessly handles running your code either in a test setting where no actual network traffic is generated or in a real or "live" setting in which your code can interact with other networked systems. 7 | 8 | The Switchyard runtime environment (depicted below) provides a given networked system with 1 or more *interfaces* or *ports*. A port may represent a wired connection to another device, or may represent a wireless interface, or may represent a *loopback* [#loopback]_ interface. In any case, it is through these ports that packets are sent and received. Each port has, at minimum, a name (e.g., ``en0``) and an Ethernet address. A port may also have an IPv4 address and subnet mask associated with it. 9 | 10 | 11 | .. figure:: srpyarch.* 12 | :align: center 13 | :figwidth: 80% 14 | 15 | 16 | The typical goal of a Switchyard-based program is to receive a packet on one port, possibly modify it, then either forward it out one or more ports or to drop the packet. The rest of this documentation is organized around how to perform these tasks in various settings. In particular: 17 | 18 | * The next section, :ref:`writing a Switchyard program `, describes how to develop a basic Switchyard program, including what APIs are available for parsing and constructing packets and sending/receiving packets on network interfaces. 19 | * Following that, the next section, :ref:`running in the test environment `, provides details on running a Switchyard program in the test environment. The section after that gives details on :ref:`how to write test scenarios`. 20 | * The next section describes :ref:`running Switchyard in a live environment `, such as on a standard Linux host or within the Mininet emulation environment or some other kind of virtual environment. 21 | * :ref:`Advanced API topics ` are addressed next, such as creating new packet header types, and implementing network protocol stacks that can interoperate with a Python socket-based program. 22 | * An :ref:`installation guide ` appears next. 23 | * Finally, you can find an :ref:`apiref` at the end of this documentation along with and an index. 24 | 25 | **A note to the pedantic**: In this documentation we use the term *packet* in a generic sense to refer to what may more traditionally be a link layer *frame*, a network layer *packet*, a transport layer *segment*, or an application layer *message*. Where appropriate, we use the appropriate specific term, but often resort to using *packet* in a more general sense. 26 | 27 | **And one more (genuinely important) note**: Switchyard is Python 3-only! You'll get an error (or maybe even more than one error!) if you try to use Switchyard with Python 2. Python 3.4 is required, at minimum. An installation guide (see :ref:`install`) is provided in this documentation to help with getting any necessary libraries installed on your platform to make Switchyard work right. 28 | 29 | 30 | .. rubric:: Footnotes 31 | 32 | .. [#loopback] The loopback interface is a *virtual* interface that connects a host to itself. It is typically used to facilitate network communication among processes on the same host. 33 | 34 | 35 | -------------------------------------------------------------------------------- /switchyard/swyard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | sys.path.append(os.getcwd()) 6 | import argparse 7 | 8 | from switchyard.syinit import start_framework 9 | import switchyard.textcolor as tcolor 10 | 11 | def version_check(): 12 | required = (3,6) 13 | this = (sys.version_info.major, sys.version_info.minor) 14 | if this < required: 15 | log_failure("Invalid Python version for using Switchyard: need at least 3.6") 16 | sys.exit(-1) 17 | 18 | def main(): 19 | version_check() 20 | 21 | progname = "swyard" 22 | parser = argparse.ArgumentParser(prog=progname) 23 | parser.add_argument("-i", "--include", metavar='INCLUDE_INTF', 24 | help="Specify interface names to include/use for data plane traffic " 25 | "(default: all non-loopback interfaces).", 26 | dest="intf", action='append') 27 | parser.add_argument("-x", "--exclude", metavar='EXCLUDE_INTF', 28 | help="Specify interface names to exclude in {}. " 29 | "All other non-loopback interfaces will be used.".format(progname), 30 | dest="exclude", action='append') 31 | parser.add_argument('usercode', metavar="YOURCODE", type=str, nargs='?', 32 | help='User switch/router code to execute.') 33 | parser.add_argument("-g", "--codearg", metavar="YOURCODE_ARGS", 34 | type=str, default='', 35 | help='Arguments to pass to your code (if multiple args, they need to be ' 36 | ' quoted in the shell).') 37 | parser.add_argument("-c", "--compile", 38 | help="Compile test scenario to binary format for distribution.", 39 | dest="compile", action="append", metavar="TEST_SCENARIO") 40 | parser.add_argument("-t", "--test", 41 | help="Run {} in testing mode, using the given test " 42 | "scenario file.".format(progname), metavar="TESTSCENARIO", 43 | dest="tests", action="append") 44 | parser.add_argument("--dryrun", 45 | help="Get everything ready to go, but don't actually do anything.", 46 | action='store_true', dest='dryrun', default=False) 47 | parser.add_argument("-v", "--verbose", 48 | help="Turn on verbose output, including full packet dumps in test " 49 | "results. Can be specified multiple times to increase verbosity.", 50 | dest="verbose", action="count", default=0) 51 | parser.add_argument("-d", "--debug", help="Turn on debug logging output.", 52 | dest="debug", action="store_true", default=False) 53 | parser.add_argument("-l", "--logfile", help="Specify the name of a file to send" 54 | " log entries to (default is to send log to stdout/stderr).", 55 | dest="logfile", default=None, type=str) 56 | parser.add_argument('--nocolor', default=False, action='store_true', 57 | help="Don't show colored output; use terminal defaults.") 58 | parser.add_argument("--nopdb", help="Don't enter pdb on crash.", 59 | dest="nopdb", action="store_true", default=False) 60 | parser.add_argument("-f", "--firewall", 61 | help="Specify host firewall rules (for real/live mode only).", 62 | dest="fwconfig", action="append") 63 | parser.add_argument("-a", "--app", 64 | help="Specify application layer (socket-based) program to start.", 65 | dest="app", default=None, metavar="SOCKET_APP") 66 | parser.add_argument("-e", "--nohandle", 67 | help="Don't trap exceptions. Use of this option is helpful if you want" 68 | " to use Switchyard with a different symbolic debugger than pdb.", 69 | dest="nohandle", action="store_true", default=False) 70 | parser.add_argument("--cli", help="Enter switchyard simulation command-line (EXPERIMENTAL!)", 71 | dest="cli", action="store_true", default=False) 72 | parser.add_argument("--topology", help="Specify topology to use for simulation" 73 | " (only used if --cli is specified).", 74 | dest="topology", type=str, default=None) 75 | parser.add_argument("--listif", help="List available interfaces (then exit)", 76 | dest="listif", action="store_true", default=False) 77 | args = parser.parse_args() 78 | 79 | if (args.usercode is None and not args.compile) and not args.listif: 80 | parser.print_usage() 81 | return -1 82 | tcolor.TextColor.setup(args.nocolor) 83 | start_framework(args) 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /docs/_modules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Overview: module code — Switchyard 1.0.1 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 |
31 | 59 | 73 |
74 |
75 | 88 | 92 | 93 | -------------------------------------------------------------------------------- /switchyard/sim/nodeexec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import heapq 3 | from collections import namedtuple, defaultdict 4 | import threading 5 | from queue import Queue,Empty 6 | import time 7 | from importlib import import_module 8 | from cmd import Cmd 9 | import re 10 | from abc import ABCMeta,abstractmethod 11 | 12 | from ..llnetbase import LLNetBase 13 | from ..lib.exceptions import NoPackets,Shutdown 14 | from ..lib.logging import log_debug 15 | from .monitor import * 16 | from ..lib.topo import * 17 | from .linkem import LinkEmulator 18 | 19 | 20 | EgressPipe = namedtuple('EgressPipe', ['queue','delay','capacity','remote_devname']) 21 | class NodeExecutor(LLNetBase): 22 | __slots__ = ['__done', '__ingress_queue', '__egress_pipes', '__name','__interfaces','__symod', '__linkem', '__tolinkem','__recv_monitors','__t'] 23 | def __init__(self, name, ingress_queue, symod=None): 24 | LLNetBase.__init__(self) 25 | self.__ingress_queue = ingress_queue 26 | self.__egress_pipes = {} 27 | self.__name = name 28 | self.__interfaces = {} 29 | self.__symod = symod 30 | self.__done = False 31 | self.__linkem = None 32 | self.__tolinkem = None 33 | self.__recv_monitors = {'host': NullMonitor()} 34 | self.__t = None 35 | 36 | def sendHostPacket(self, pkt): 37 | self.__ingress_queue.put( ('host', pkt) ) 38 | 39 | def addEgressInterface(self, devname, intf, queue, capacity, delay, remote_devname): 40 | # print ("Adding egress interface on {} {}".format(self.name, devname)) 41 | self.__egress_pipes[devname] = EgressPipe(queue, delay, capacity, remote_devname) 42 | self.__interfaces[devname] = intf 43 | self.__recv_monitors[devname] = NullMonitor() 44 | 45 | @property 46 | def name(self): 47 | return self.__name 48 | 49 | def interfaces(self): 50 | return self.__interfaces.values() 51 | 52 | def set_devupdown_callback(self, callback): 53 | pass 54 | 55 | def interface_by_name(self, name): 56 | return self.__interfaces[name] 57 | 58 | def interface_by_ipaddr(self, ipaddr): 59 | pass 60 | 61 | def interface_by_macaddr(self, macaddr): 62 | pass 63 | 64 | def attach_recv_monitor(self, interface, monitorobject): 65 | self.__recv_monitors[interface] = monitorobject 66 | 67 | def remove_recv_monitor(self, interface): 68 | self.__recv_monitors[interface] = NullMonitor() 69 | 70 | def recv_packet(self, timeout=0.0, timestamp=False): 71 | # 72 | # FIXME: not sure about how best to handle... 73 | # 74 | giveup_time = time.time() + timeout 75 | inner_timeout = 0.05 76 | 77 | while timeout == 0.0 or time.time() < giveup_time: 78 | try: 79 | devname,packet = self.__ingress_queue.get(block=True, timeout=inner_timeout) 80 | now = time.time() 81 | self.__recv_monitors[devname](devname,now,packet) 82 | if timestamp: 83 | return devname,now,packet 84 | return devname,packet 85 | except Empty: 86 | pass 87 | 88 | if self.__done: 89 | raise Shutdown() 90 | 91 | raise NoPackets() 92 | 93 | def send_packet(self, dev, packet): 94 | egress_pipe = self.__egress_pipes[dev] 95 | now = time.time() 96 | delay = now + len(packet) / float(egress_pipe.capacity) + egress_pipe.delay 97 | self.__tolinkem.put( (delay, (egress_pipe.remote_devname, packet), egress_pipe.queue) ) 98 | 99 | def shutdown(self): 100 | self.__linkem.shutdown() 101 | self.__done = True 102 | log_debug("Joining node codeexec thread {} node {}".format(self.__t.name, self.__name)) 103 | self.__t.join() 104 | 105 | def __idleloop(self): 106 | while not self.__done: 107 | try: 108 | devname,ts,packet = self.recv_packet(timestamp=True, timeout=0.1) 109 | except Shutdown: 110 | break 111 | except NoPackets: 112 | pass 113 | 114 | def run(self): 115 | self.__tolinkem = Queue() 116 | self.__linkem = LinkEmulator(self.__tolinkem) 117 | self.__t = threading.Thread(target=self.__linkem.run) 118 | self.__t.start() 119 | self.startcode() 120 | 121 | def resetcode(self, mod=None): 122 | self.__symod = mod 123 | self.startcode() 124 | 125 | def startcode(self): 126 | if self.__symod: 127 | self.__symod(self) 128 | else: 129 | self.__idleloop() 130 | self.__t.join() 131 | del self.__t 132 | del self.__linkem 133 | del self.__tolinkem 134 | del self.__egress_pipes 135 | del self.__recv_monitors 136 | 137 | -------------------------------------------------------------------------------- /tests/test_hostfirewall.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | import switchyard.hostfirewall as hf 5 | 6 | class HostFirewallTests(unittest.TestCase): 7 | def _collectcmd(self, cmd, stdin=None): 8 | self.cmds.append( (cmd,stdin) ) 9 | rv = "" 10 | if cmd == "/sbin/pfctl -E": 11 | rv = "Token: 0" 12 | return True,rv 13 | 14 | def setUp(self): 15 | self.cmds = [] 16 | setattr(hf, "_runcmd", self._collectcmd) 17 | hf.Firewall._instance = None 18 | 19 | def testErrorInit(self): 20 | hf.Firewall(("eth0",), ("icmp:*","tcp:80")) 21 | with self.assertRaises(Exception): 22 | hf.Firewall(("eth0",), ("icmp:*","tcp:80")) 23 | setattr(sys, "platform", "vax/vms") 24 | hf.Firewall._instance = None 25 | with self.assertRaises(Exception): 26 | hf.Firewall(("eth0",), ("icmp:*","tcp:80")) 27 | 28 | def testLinux(self): 29 | setattr(sys, "platform", "linux") 30 | fw = hf.Firewall(("eth0",), ("icmp:*","tcp:80")) 31 | fw.__enter__() 32 | cmds = ['/sbin/iptables-save', 33 | '/sbin/iptables -F', 34 | '/sbin/iptables -t raw -F', 35 | '/sbin/iptables -t raw -A PREROUTING -j DROP --protocol icmp -i eth0', 36 | '/sbin/iptables -t raw -A PREROUTING -j DROP --protocol tcp -i eth0 --dport 80'] 37 | 38 | xcmds = [ c for c,inp in self.cmds] 39 | self.assertEqual(cmds, xcmds) 40 | fw.add_rule("udp:123") 41 | cmds = ['/sbin/iptables-save', 42 | '/sbin/iptables -F', 43 | '/sbin/iptables -t raw -F', 44 | '/sbin/iptables -t raw -A PREROUTING -j DROP --protocol icmp -i eth0', 45 | '/sbin/iptables -t raw -A PREROUTING -j DROP --protocol tcp -i eth0 --dport 80', 46 | '/sbin/iptables -t raw -A PREROUTING -j DROP --protocol udp -i eth0 --dport 123'] 47 | xcmds = [ c for c,inp in self.cmds] 48 | self.assertEqual(cmds, xcmds) 49 | fw.__exit__(0,0,0) 50 | 51 | def testLinux2(self): 52 | setattr(sys, "platform", "linux") 53 | fw = hf.Firewall(("eth0","eth1"), ("all",)) 54 | fw.__enter__() 55 | cmds = ['/sbin/iptables-save', 56 | '/sbin/sysctl net.ipv4.conf.eth0.arp_ignore', 57 | '/sbin/sysctl net.ipv4.conf.eth1.arp_ignore', 58 | '/sbin/iptables -F', 59 | '/sbin/iptables -t raw -F', 60 | '/sbin/iptables -t raw -A PREROUTING -j DROP -i eth0', 61 | '/sbin/iptables -t raw -A PREROUTING -j DROP -i eth1',] 62 | xcmds = [ c for c,inp in self.cmds] 63 | self.assertEqual(cmds, xcmds) 64 | fw.__exit__(0,0,0) 65 | 66 | def testLinux3(self): 67 | setattr(sys, "platform", "linux") 68 | fw = hf.Firewall(("eth0","eth1"), ("none",)) 69 | fw.__enter__() 70 | cmds = ['/sbin/iptables-save', 71 | '/sbin/iptables -F', 72 | '/sbin/iptables -t raw -F' ] 73 | xcmds = [ c for c,inp in self.cmds] 74 | self.assertEqual(cmds, xcmds) 75 | fw.__exit__(0,0,0) 76 | 77 | def testMacos(self): 78 | setattr(sys, "platform", "darwin") 79 | fw = hf.Firewall(("eth0",), ("icmp:*","tcp:80")) 80 | fw.__enter__() 81 | rules = self.cmds[1][1] 82 | self.assertEqual(rules[0], 'block drop on eth0 proto icmp from any to any') 83 | self.assertEqual(rules[1], 'block drop on eth0 proto tcp from any port 80 to any port 80') 84 | fw.add_rule("udp:123") 85 | rules = self.cmds[1][1] 86 | self.assertEqual(rules[0], 'block drop on eth0 proto icmp from any to any') 87 | self.assertEqual(rules[1], 'block drop on eth0 proto tcp from any port 80 to any port 80') 88 | self.assertEqual(rules[2], 'block drop on eth0 proto udp from any port 123 to any port 123') 89 | fw.__exit__(0,0,0) 90 | 91 | def testBadRule1(self): 92 | setattr(sys, "platform", "test") 93 | with self.assertRaises(ValueError): 94 | fw = hf.Firewall(("eth0",), ("mpxyz",)) 95 | 96 | def testBadRule2(self): 97 | setattr(sys, "platform", "test") 98 | with self.assertRaises(ValueError): 99 | fw = hf.Firewall(("eth0",), ("xicmp:asdf",)) 100 | 101 | def testTest(self): 102 | setattr(sys, "platform", "test") 103 | fw = hf.Firewall(("eth0",), ("all", "none", "icmp:*","tcp:80")) 104 | fw.__enter__() 105 | self.assertEqual(self.cmds, []) 106 | fw.add_rule("udp:123") 107 | self.assertEqual(self.cmds, []) 108 | self.assertEqual( 109 | [('all', None), ('none', None), ('icmp', None), ('tcp', '80'), ('udp', '123')], 110 | hf.Firewall._instance._firewall_delegate._rules) 111 | fw.__exit__(0,0,0) 112 | 113 | 114 | if __name__ == '__main__': 115 | unittest.main() 116 | 117 | -------------------------------------------------------------------------------- /switchyard/lib/topo/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def humanize_bandwidth(bits): 4 | ''' 5 | Accept some number of bits/sec (i.e., a link capacity) as an 6 | integer, and return a string representing a 'human'(-like) 7 | representation of the capacity, e.g., 10 Mb/s, 1.5 Mb/s, 8 | 900 Gb/s. 9 | 10 | As is the standard in networking, capacity values are assumed 11 | to be base-10 values (not base 2), so 1000 is 1 Kb/s. 12 | ''' 13 | unit = '' 14 | divisor = 1 15 | if bits < 1000: 16 | unit = 'bits' 17 | divisor = 1 18 | elif bits < 1000000: 19 | unit = 'Kb' 20 | divisor = 1000 21 | elif bits < 1000000000: 22 | unit = 'Mb' 23 | divisor = 1000000 24 | elif bits < 1000000000000: 25 | unit = 'Gb' 26 | divisor = 1000000000 27 | elif bits < 1000000000000000: 28 | unit = 'Tb' 29 | divisor = 1000000000000 30 | else: 31 | raise Exception("Can't humanize that many bits.") 32 | 33 | if bits % divisor == 0: 34 | value = int(bits/divisor) 35 | else: 36 | value = bits/divisor 37 | 38 | return "{} {}/s".format(value, unit) 39 | 40 | def unhumanize_bandwidth(bitsstr): 41 | ''' 42 | Take a string representing a link capacity, e.g., 10 Mb/s, and 43 | return an integer representing the number of bits/sec. 44 | Recognizes: 45 | - 'bits/sec' or 'b/s' are treated as plain bits per second 46 | - 'Kb' or 'kb' as thousand bits/sec 47 | - 'Mb' or 'mb' as million bits/sec 48 | - 'Gb' or 'gb' as billion bits/sec 49 | - 'Tb' or 'tb' as trillion bits/sec 50 | - if second character is 'B', quantity is interpreted as bytes/sec 51 | - any subsequent characters after the first two are ignored, so 52 | Kb/s Kb/sec Kbps are interpreted identically. 53 | 54 | Returns None if the string doesn't contain anything parseable. 55 | ''' 56 | if isinstance(bitsstr, int): 57 | return bitsstr 58 | 59 | mobj = re.match('^\s*([\d\.]+)\s*(.*)\s*$', bitsstr) 60 | if not mobj: 61 | return None 62 | value, units = mobj.groups() 63 | value = float(value) 64 | multipliers = { 'b':1, 'k':1e3, 'm':1e6, 'g':1e9, 't':1e12 } 65 | if not units: 66 | units = 'bits' 67 | mult = multipliers.get(units[0].lower(), 0) 68 | bits = 1 69 | if len(units) > 1: 70 | if units[1] == 'B': bits = 8 71 | # print (bitsstr, value, mult, bits) 72 | return int(value * mult * bits) 73 | 74 | # a couple aliases 75 | humanize_capacity = humanize_bandwidth 76 | unhumanize_capacity = unhumanize_bandwidth 77 | 78 | def humanize_delay(delay): 79 | ''' 80 | Accept a floating point number presenting link propagation delay 81 | in seconds (e.g., 0.1 for 100 milliseconds delay), and return 82 | a human(-like) string like '100 milliseconds'. Handles values as 83 | small as 1 microsecond, but no smaller. 84 | 85 | Because of imprecision in floating point numbers, a relatively easy 86 | way to handle this is to convert to string, then slice out sections. 87 | ''' 88 | delaystr = '{:1.06f}'.format(delay) 89 | decimal = delaystr.find('.') 90 | seconds = int(delaystr[:decimal]) 91 | millis = int(delaystr[-6:-3]) 92 | micros = int(delaystr[-3:]) 93 | # print (delay,delaystr,seconds,millis,micros) 94 | units = '' 95 | microsecs = micros + 1e3 * millis + 1e6 * seconds 96 | if micros > 0: 97 | units = ' \u00B5sec' 98 | value = int(microsecs) 99 | elif millis > 0: 100 | units = ' msec' 101 | value = int(microsecs / 1000) 102 | elif seconds > 0: 103 | units = ' sec' 104 | value = int(microsecs / 1000000) 105 | else: 106 | units = ' sec' 107 | value = delay 108 | if value > 1: 109 | units += 's' 110 | return '{}{}'.format(value, units) 111 | 112 | def unhumanize_delay(delaystr): 113 | ''' 114 | Accept a string representing link propagation delay (e.g., 115 | '100 milliseconds' or '100 msec' or 100 millisec') and return 116 | a floating point number representing the delay in seconds. 117 | Recognizes: 118 | - us, usec, micros* all as microseconds 119 | - ms, msec, millisec* all as milliseconds 120 | - s, sec* as seconds 121 | 122 | returns None on parse failure. 123 | ''' 124 | if isinstance(delaystr, float): 125 | return delaystr 126 | 127 | mobj = re.match('^\s*([\d\.]+)\s*(\w*)', delaystr) 128 | if not mobj: 129 | return None 130 | value, units = mobj.groups() 131 | value = float(value) 132 | if not units: 133 | divisor = 1.0 134 | elif units == 'us' or units == 'usec' or units.startswith('micros'): 135 | divisor = 1e6 136 | elif units == 'ms' or units == 'msec' or units.startswith('millis'): 137 | divisor = 1e3 138 | elif units == 's' or units.startswith('sec'): 139 | divisor = 1.0 140 | else: 141 | return None 142 | return value / divisor 143 | -------------------------------------------------------------------------------- /docs/_static/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .highlight .hll { background-color: #ffffcc } 7 | .highlight { background: #eeffcc; } 8 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 9 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 10 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 11 | .highlight .o { color: #666666 } /* Operator */ 12 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 18 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 19 | .highlight .ge { font-style: italic } /* Generic.Emph */ 20 | .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ 21 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 22 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 23 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 24 | .highlight .go { color: #333333 } /* Generic.Output */ 25 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 26 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 27 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 28 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 29 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 30 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 31 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 32 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 33 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 34 | .highlight .kt { color: #902000 } /* Keyword.Type */ 35 | .highlight .m { color: #208050 } /* Literal.Number */ 36 | .highlight .s { color: #4070a0 } /* Literal.String */ 37 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 38 | .highlight .nb { color: #007020 } /* Name.Builtin */ 39 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 40 | .highlight .no { color: #60add5 } /* Name.Constant */ 41 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 42 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 43 | .highlight .ne { color: #007020 } /* Name.Exception */ 44 | .highlight .nf { color: #06287e } /* Name.Function */ 45 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 46 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 47 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 48 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 49 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 50 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 51 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 52 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 53 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 54 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 55 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 56 | .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ 57 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 58 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 59 | .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ 60 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 61 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 62 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 63 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 64 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 65 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 66 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 67 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 68 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 69 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 70 | .highlight .fm { color: #06287e } /* Name.Function.Magic */ 71 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 72 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 73 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 74 | .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ 75 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /switchyard/lib/packet/arp.py: -------------------------------------------------------------------------------- 1 | from .packet import PacketHeaderBase,Packet 2 | from ..address import EthAddr,ip_address,SpecialIPv4Addr,SpecialEthAddr 3 | import struct 4 | from .common import EtherType, ArpHwType, ArpOperation 5 | from ..exceptions import * 6 | 7 | ''' 8 | References: 9 | Plummer. 10 | "RFC826", An Ethernet Address Resolution Protocol. 11 | Finlayson, Mann, Mogul, and Theimer. 12 | "RFC903", A Reverse Address Resolution Protocol. 13 | http://en.wikipedia.org/wiki/Address_Resolution_Protocol 14 | ''' 15 | 16 | class Arp(PacketHeaderBase): 17 | __slots__ = ['_hwtype','_prototype','_hwaddrlen','_protoaddrlen', 18 | '_operation','_senderhwaddr','_senderprotoaddr', 19 | '_targethwaddr','_targetprotoaddr'] 20 | _PACKFMT = '!HHBBH6s4s6s4s' 21 | _MINLEN = struct.calcsize(_PACKFMT) 22 | 23 | def __init__(self, **kwargs): 24 | self._hwtype = ArpHwType.Ethernet 25 | self._prototype = EtherType.IP 26 | self._hwaddrlen = 6 27 | self._protoaddrlen = 4 28 | self.operation = ArpOperation.Request 29 | self.senderhwaddr = SpecialEthAddr.ETHER_ANY.value 30 | self.senderprotoaddr = SpecialIPv4Addr.IP_ANY.value 31 | self.targethwaddr = SpecialEthAddr.ETHER_BROADCAST.value 32 | self.targetprotoaddr = SpecialIPv4Addr.IP_ANY.value 33 | super().__init__(**kwargs) 34 | 35 | def size(self): 36 | return struct.calcsize(Arp._PACKFMT) 37 | 38 | def pre_serialize(self, raw, pkt, i): 39 | pass 40 | 41 | def to_bytes(self): 42 | ''' 43 | Return packed byte representation of the ARP header. 44 | ''' 45 | return struct.pack(Arp._PACKFMT, self._hwtype.value, self._prototype.value, self._hwaddrlen, self._protoaddrlen, self._operation.value, self._senderhwaddr.packed, self._senderprotoaddr.packed, self._targethwaddr.packed, self._targetprotoaddr.packed) 46 | 47 | def from_bytes(self, raw): 48 | '''Return an Ethernet object reconstructed from raw bytes, or an 49 | Exception if we can't resurrect the packet.''' 50 | if len(raw) < Arp._MINLEN: 51 | raise NotEnoughDataError("Not enough bytes ({}) to reconstruct an Arp object".format(len(raw))) 52 | fields = struct.unpack(Arp._PACKFMT, raw[:Arp._MINLEN]) 53 | try: 54 | self._hwtype = ArpHwType(fields[0]) 55 | self._prototype = EtherType(fields[1]) 56 | self._hwaddrlen = fields[2] 57 | self._protoaddrlen = fields[3] 58 | self.operation = ArpOperation(fields[4]) 59 | self.senderhwaddr = EthAddr(fields[5]) 60 | self.senderprotoaddr = ip_address(fields[6]) 61 | self.targethwaddr = EthAddr(fields[7]) 62 | self.targetprotoaddr = ip_address(fields[8]) 63 | except Exception as e: 64 | raise Exception("Error constructing Arp packet object from raw bytes: {}".format(str(e))) 65 | return raw[Arp._MINLEN:] 66 | 67 | def __eq__(self, other): 68 | return self.hardwaretype == other.hardwaretype and \ 69 | self.protocoltype == other.protocoltype and \ 70 | self.operation == other.operation and \ 71 | self.senderhwaddr == other.senderhwaddr and \ 72 | self.senderprotoaddr == other.senderprotoaddr and \ 73 | self.targethwaddr == other.targethwaddr and \ 74 | self.targetprotoaddr == other.targetprotoaddr 75 | 76 | @property 77 | def hardwaretype(self): 78 | return self._hwtype 79 | 80 | @property 81 | def protocoltype(self): 82 | return self._prototype 83 | 84 | @property 85 | def operation(self): 86 | return self._operation 87 | 88 | @operation.setter 89 | def operation(self, value): 90 | self._operation = ArpOperation(value) 91 | 92 | @property 93 | def senderhwaddr(self): 94 | return self._senderhwaddr 95 | 96 | @senderhwaddr.setter 97 | def senderhwaddr(self, value): 98 | self._senderhwaddr = EthAddr(value) 99 | 100 | @property 101 | def senderprotoaddr(self): 102 | return self._senderprotoaddr 103 | 104 | @senderprotoaddr.setter 105 | def senderprotoaddr(self, value): 106 | self._senderprotoaddr = ip_address(value) 107 | 108 | @property 109 | def targethwaddr(self): 110 | return self._targethwaddr 111 | 112 | @targethwaddr.setter 113 | def targethwaddr(self, value): 114 | self._targethwaddr = EthAddr(value) 115 | 116 | @property 117 | def targetprotoaddr(self): 118 | return self._targetprotoaddr 119 | 120 | @targetprotoaddr.setter 121 | def targetprotoaddr(self, value): 122 | self._targetprotoaddr = ip_address(value) 123 | 124 | def next_header_class(self): 125 | ''' 126 | No other headers should follow ARP. 127 | ''' 128 | return None 129 | 130 | def __str__(self): 131 | return '{} {}:{} {}:{}'.format(self.__class__.__name__, 132 | self.senderhwaddr, self.senderprotoaddr, 133 | self.targethwaddr, self.targetprotoaddr) 134 | -------------------------------------------------------------------------------- /docs/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Base JavaScript utilities for all Sphinx HTML documentation. 6 | * 7 | * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | "use strict"; 12 | 13 | const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ 14 | "TEXTAREA", 15 | "INPUT", 16 | "SELECT", 17 | "BUTTON", 18 | ]); 19 | 20 | const _ready = (callback) => { 21 | if (document.readyState !== "loading") { 22 | callback(); 23 | } else { 24 | document.addEventListener("DOMContentLoaded", callback); 25 | } 26 | }; 27 | 28 | /** 29 | * Small JavaScript module for the documentation. 30 | */ 31 | const Documentation = { 32 | init: () => { 33 | Documentation.initDomainIndexTable(); 34 | Documentation.initOnKeyListeners(); 35 | }, 36 | 37 | /** 38 | * i18n support 39 | */ 40 | TRANSLATIONS: {}, 41 | PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), 42 | LOCALE: "unknown", 43 | 44 | // gettext and ngettext don't access this so that the functions 45 | // can safely bound to a different name (_ = Documentation.gettext) 46 | gettext: (string) => { 47 | const translated = Documentation.TRANSLATIONS[string]; 48 | switch (typeof translated) { 49 | case "undefined": 50 | return string; // no translation 51 | case "string": 52 | return translated; // translation exists 53 | default: 54 | return translated[0]; // (singular, plural) translation tuple exists 55 | } 56 | }, 57 | 58 | ngettext: (singular, plural, n) => { 59 | const translated = Documentation.TRANSLATIONS[singular]; 60 | if (typeof translated !== "undefined") 61 | return translated[Documentation.PLURAL_EXPR(n)]; 62 | return n === 1 ? singular : plural; 63 | }, 64 | 65 | addTranslations: (catalog) => { 66 | Object.assign(Documentation.TRANSLATIONS, catalog.messages); 67 | Documentation.PLURAL_EXPR = new Function( 68 | "n", 69 | `return (${catalog.plural_expr})` 70 | ); 71 | Documentation.LOCALE = catalog.locale; 72 | }, 73 | 74 | /** 75 | * helper function to focus on search bar 76 | */ 77 | focusSearchBar: () => { 78 | document.querySelectorAll("input[name=q]")[0]?.focus(); 79 | }, 80 | 81 | /** 82 | * Initialise the domain index toggle buttons 83 | */ 84 | initDomainIndexTable: () => { 85 | const toggler = (el) => { 86 | const idNumber = el.id.substr(7); 87 | const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); 88 | if (el.src.substr(-9) === "minus.png") { 89 | el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; 90 | toggledRows.forEach((el) => (el.style.display = "none")); 91 | } else { 92 | el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; 93 | toggledRows.forEach((el) => (el.style.display = "")); 94 | } 95 | }; 96 | 97 | const togglerElements = document.querySelectorAll("img.toggler"); 98 | togglerElements.forEach((el) => 99 | el.addEventListener("click", (event) => toggler(event.currentTarget)) 100 | ); 101 | togglerElements.forEach((el) => (el.style.display = "")); 102 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); 103 | }, 104 | 105 | initOnKeyListeners: () => { 106 | // only install a listener if it is really needed 107 | if ( 108 | !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && 109 | !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS 110 | ) 111 | return; 112 | 113 | document.addEventListener("keydown", (event) => { 114 | // bail for input elements 115 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; 116 | // bail with special keys 117 | if (event.altKey || event.ctrlKey || event.metaKey) return; 118 | 119 | if (!event.shiftKey) { 120 | switch (event.key) { 121 | case "ArrowLeft": 122 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 123 | 124 | const prevLink = document.querySelector('link[rel="prev"]'); 125 | if (prevLink && prevLink.href) { 126 | window.location.href = prevLink.href; 127 | event.preventDefault(); 128 | } 129 | break; 130 | case "ArrowRight": 131 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 132 | 133 | const nextLink = document.querySelector('link[rel="next"]'); 134 | if (nextLink && nextLink.href) { 135 | window.location.href = nextLink.href; 136 | event.preventDefault(); 137 | } 138 | break; 139 | } 140 | } 141 | 142 | // some keyboard layouts may need Shift to get / 143 | switch (event.key) { 144 | case "/": 145 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; 146 | Documentation.focusSearchBar(); 147 | event.preventDefault(); 148 | } 149 | }); 150 | }, 151 | }; 152 | 153 | // quick alias for translations 154 | const _ = Documentation.gettext; 155 | 156 | _ready(Documentation.init); 157 | -------------------------------------------------------------------------------- /switchyard/lib/interface.py: -------------------------------------------------------------------------------- 1 | from ipaddress import ip_interface, ip_address, IPv6Interface, IPv4Interface, IPv6Address, IPv4Address 2 | from enum import Enum 3 | from socket import if_nametoindex 4 | 5 | from .address import EthAddr 6 | from .logging import log_debug 7 | from ..pcapffi import pcap_devices 8 | 9 | class InterfaceType(Enum): 10 | Unknown=1 11 | Loopback=2 12 | Wired=3 13 | Wireless=4 14 | 15 | class Interface(object): 16 | __slots__ = ['__name','__ethaddr','__ipaddrset','__ifnum','__iftype'] 17 | __nextnum = 1 18 | 19 | ''' 20 | Class that models a single logical interface on a network 21 | device. An interface has a name, 48-bit Ethernet MAC address, 22 | and (optionally) set of IP addresses with network masks. An interface 23 | also has a number associated with it and a type, which is one 24 | of the values of the enumerated type ``InterfaceType``. 25 | ''' 26 | def __init__(self, name, ethaddr, ifnum=None, iftype=InterfaceType.Unknown): 27 | self.__name = name 28 | self.ethaddr = ethaddr 29 | self.__ipaddrset = set() 30 | self.ifnum = ifnum 31 | self.__iftype = iftype 32 | 33 | @property 34 | def name(self): 35 | '''Get the name of the interface''' 36 | return self.__name 37 | 38 | @property 39 | def ethaddr(self): 40 | '''Get the Ethernet address associated with the interface''' 41 | return self.__ethaddr 42 | 43 | @ethaddr.setter 44 | def ethaddr(self, value): 45 | if isinstance(value, EthAddr): 46 | self.__ethaddr = value 47 | elif isinstance(value, (str,bytes)): 48 | self.__ethaddr = EthAddr(value) 49 | elif value is None: 50 | self.__ethaddr = EthAddr('00:00:00:00:00:00') 51 | else: 52 | raise ValueError("Can't initialize ethaddr with {}".format(value)) 53 | 54 | @property 55 | def ipaddrs(self): 56 | '''Get the IP addresses associated with the interface, as a (frozen) set 57 | of ipaddress.IPv4Interface or ipaddress.IPv6Interface objects''' 58 | return frozenset(self.__ipaddrset) 59 | 60 | def assign_ipaddr(self, value): 61 | ''' 62 | Assign a new IP address (v4 or v6) to this interface. 63 | Address can either be a IPv4Interface object, IPv6Interface object, 64 | or string in the form 'addr/prefix' (i.e., something that 65 | ipaddress.ip_interface will parse). 66 | ''' 67 | if isinstance(value, (IPv4Interface, IPv6Interface)): 68 | self.__ipaddrset.add(value) 69 | elif isinstance(value, (str,IPv4Address,IPv6Address)): 70 | self.__ipaddrset.add(ip_interface(value)) 71 | elif value is None: 72 | self.__ipaddrset.add(ip_interface('0.0.0.0')) 73 | else: 74 | raise Exception("Invalid type assignment to IP address (must be string or existing IP address) ({})".format(value)) 75 | 76 | def remove_ipaddr(self, value): 77 | ''' 78 | Remove an IP address from this interface. Value to remove 79 | can be as a string or IPAddress object. 80 | ''' 81 | ipa = ip_interface(value) 82 | for addr in self.__ipaddrset: 83 | if addr.ip == ipa.ip: 84 | self.__ipaddrset.remove(addr) 85 | return 86 | raise Exception("No such address {} exists to remove from interface".format(ipa)) 87 | 88 | @property 89 | def ifnum(self): 90 | '''Get the interface number (integer) associated with the interface''' 91 | return self.__ifnum 92 | 93 | @ifnum.setter 94 | def ifnum(self, value): 95 | if not isinstance(value, int): 96 | value = Interface.__nextnum 97 | Interface.__nextnum += 1 98 | self.__ifnum = int(value) 99 | 100 | @property 101 | def iftype(self): 102 | '''Get the type of the interface as a value from the InterfaceType enumeration.''' 103 | return self.__iftype 104 | 105 | def __str__(self): 106 | s = "{} mac:{}".format(str(self.name), str(self.ethaddr)) 107 | for ipa in self.ipaddrs: 108 | if int(ipa) != 0: 109 | s += " ip:{}".format(ipa) 110 | return s 111 | 112 | def make_device_list(includes=set(), excludes=set()): 113 | log_debug("Making device list. Includes: {}, Excludes: {}".format(includes, excludes)) 114 | non_interfaces = set() 115 | devs = set([ dev.name for dev in pcap_devices() if not dev.isloop or dev.name in includes]) 116 | includes = set(includes) # may have been given as a list 117 | includes.intersection_update(devs) # only include devs that actually exist 118 | 119 | for d in devs: 120 | try: 121 | ifnum = if_nametoindex(d) 122 | except: 123 | non_interfaces.add(d) 124 | devs.difference_update(non_interfaces) 125 | log_debug("Devices found: {}".format(devs)) 126 | 127 | # remove devs from excludelist 128 | devs.difference_update(set(excludes)) 129 | 130 | # if includelist is non-empty, perform 131 | # intersection with devs found and includelist 132 | if includes: 133 | devs.intersection_update(includes) 134 | 135 | log_debug("Using these devices: {}".format(devs)) 136 | return devs 137 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Switchyard 2 | ========== 3 | 4 | Switchyard is a library and framework for implementing software-based networked systems in Python such as Ethernet switches, IP routers, middleboxes, and end-host protocol stacks. It is primarily intended for educational use and supports creating devices from layer 2 (link layer) all the way through the application layer. 5 | 6 | Documentation is available at https://jsommers.github.io/switchyard. Documentation is written using the Python Sphinx package; doc sources are available in the documentation directory. 7 | 8 | Switchyard can run in a standalone test harness mode or can also use "live" network interfaces on a Linux or macOS host. It works nicely within Mininet and other virtual host environments, too. 9 | 10 | Some history 11 | ^^^^^^^^^^^^ 12 | 13 | The version of Switchyard on the master branch is a major revision of the code as of July 2020. There are non-backward compatible API changes (particularly with the ``Interface`` class). Please see the release notes for more info. If you need an earlier version, use ``pip3 install switchyard==2019.1.1``. 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | Prequisites: you'll also likely need to install additional packages (do this before using pip to install the Python libraries) on different operating systems: 20 | 21 | * Fedora (20 and later): ``sudo yum install libffi-devel libpcap-devel python3-devel python3-pip python3-virtualenv`` 22 | * FreeBSD 11 and later: ``pkg install libffi libpcap`` 23 | * macOS: install libffi and libpcap through mac Homebrew (https://brew.sh) 24 | * Ubuntu (14.04 and later): ``sudo apt-get install libffi-dev libpcap-dev python3-dev python3-pip python3-venv`` 25 | 26 | You can install Switchyard and the necessary related packages in an isolated Python virtual environment ("venv"), which is the recommended path, or in the system directories, which is often less desirable. The venv route is highly suggested, since it makes all installation "local" and can easily destroyed, cleaned up, and recreated. 27 | 28 | To create a new virtual environment, you could do something like the following:: 29 | 30 | $ python3 -m venv syenv 31 | 32 | You can change the name *syenv* to whatever you'd like to name your virtual environment. Next, you need to activate the environment. The instructions vary depending on the shell you're using. On ``bash``, the command is:: 33 | 34 | $ source ./syenv/bin/activate 35 | 36 | You'll need to replace *syenv* with whatever you named the virtual environment. If you're using a different shell than bash, refer to Python documentation on the venv module. 37 | 38 | Finally, install Switchyard. All the required additional libraries should be automatically installed, too. 39 | 40 | :: 41 | 42 | $ python3 -m pip install switchyard 43 | 44 | 45 | There is also a docker image, if you prefer: https://hub.docker.com/repository/docker/jsommers/switchyard. Just do:: 46 | 47 | $ docker run -v `pwd`:/swyard -it jsommers/switchyard 48 | 49 | --- 50 | 51 | Results of tests run in Travis (on Ubuntu): 52 | 53 | .. image:: https://travis-ci.org/jsommers/switchyard.svg?branch=master 54 | :target: https://travis-ci.org/jsommers/switchyard 55 | 56 | 57 | Documentation and Exercises 58 | --------------------------- 59 | 60 | * Documentation sources can be found in the documentation directory. See http://jsommers.github.io/switchyard for compiled/built docs in HTML. 61 | 62 | * Sample exercises (in ReStructuredText format) can be found in the examples/exercises directory. The examples are *not* included with the Switchyard package that is installed through 63 | 64 | * Instructor-only materials such as test scenarios and other scripts available on request (jsommers@colgate.edu); they're in a separate private repository. 65 | 66 | Credits 67 | ------- 68 | 69 | I gratefully acknowledge support from the NSF. The materials here are based upon work supported by the National Science Foundation under grants CNS-1814537 ("NeTS: Small: RUI: Automating Active Measurement Metadata Collection and Analysis"; 2018-2021) and CNS-1054985 ("CAREER: Expanding the functionality of Internet routers"; 2011-2017). 70 | 71 | Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author and do not necessarily reflect the views of the National Science Foundation. 72 | 73 | License 74 | ------- 75 | 76 | Copyright 2015-2020 Joel Sommers. All rights reserved. 77 | 78 | The Switchyard software is distributed under terms of the GNU General Public License, version 3. See below for the standard GNU GPL v3 copying text. 79 | 80 | Switchyard's documentation is distributed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-nc-sa/4.0/. 81 | 82 | :: 83 | 84 | This program is free software: you can redistribute it and/or modify 85 | it under the terms of the GNU General Public License as published by 86 | the Free Software Foundation, either version 3 of the License, or 87 | (at your option) any later version. 88 | 89 | This program is distributed in the hope that it will be useful, 90 | but WITHOUT ANY WARRANTY; without even the implied warranty of 91 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 92 | GNU General Public License for more details. 93 | 94 | You should have received a copy of the GNU General Public License 95 | along with this program. If not, see . 96 | -------------------------------------------------------------------------------- /docs/thanks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Acknowledgments and thanks — Switchyard 1.0.1 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |

Acknowledgments and thanks

42 |

Thanks to Colgate COSC465 students from Spring 2014 and Spring 2015 for being guinea pigs and giving feedback for the very first versions of Switchyard. Thanks also to Prof. Paul Barford and CS640 students at the University of Wisconsin for using and providing feedback on Switchyard.

43 |

Thanks to those students who have contributed fixes and made suggestions for improvements. In particular:

44 |
45 |
    46 |
  • Thanks to Saul Shanabrook for several specific suggestions and bug reports that have led to improvements in Switchyard.

  • 47 |
  • Thanks to Xuyi Ruan for identifying and suggesting a fix to bugs on one of the documentation diagrams.

  • 48 |
  • Thanks to Sean Wilson for a bug fix on an infinitely recursive property setter. Oops, but this dumb bug motivated me to significantly improve test coverage, so there’s that.

  • 49 |
  • Thanks to Leon Yang for identifying a problem with kwarg processing for ICMP.

  • 50 |
  • Thanks to Jordan Ansell at Victoria University-Wellington NZ for initial code for ICMPv6 neighbor discovery

  • 51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 |
60 | 86 |
87 |
88 | 104 | 108 | 109 | --------------------------------------------------------------------------------