diff --git a/examples/learning_switch/.gitignore b/examples/learning_switch/.gitignore new file mode 100644 index 0000000..12179ea --- /dev/null +++ b/examples/learning_switch/.gitignore @@ -0,0 +1,20 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez diff --git a/examples/learning_switch/README.md b/examples/learning_switch/README.md new file mode 100644 index 0000000..ca03145 --- /dev/null +++ b/examples/learning_switch/README.md @@ -0,0 +1,21 @@ +# LearningSwitch + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `learning_switch` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:learning_switch, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/learning_switch](https://hexdocs.pm/learning_switch). + diff --git a/examples/learning_switch/config/config.exs b/examples/learning_switch/config/config.exs new file mode 100644 index 0000000..a9393b7 --- /dev/null +++ b/examples/learning_switch/config/config.exs @@ -0,0 +1,17 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +config :tres, + protocol: :tcp, + port: 6653, + max_connections: 10, + num_acceptors: 10, + callback_module: LearningSwitch.Ofctl, + callback_args: [] + +config :logger, + level: :debug, + format: "$date $time [$level] $metadata$message\n", + metadata: [:application], + handle_otp_reports: true diff --git a/examples/learning_switch/lib/learning_switch.ex b/examples/learning_switch/lib/learning_switch.ex new file mode 100644 index 0000000..855e031 --- /dev/null +++ b/examples/learning_switch/lib/learning_switch.ex @@ -0,0 +1,18 @@ +defmodule LearningSwitch do + @moduledoc """ + Documentation for LearningSwitch. + """ + + @doc """ + Hello world. + + ## Examples + + iex> LearningSwitch.hello + :world + + """ + def hello do + :world + end +end diff --git a/examples/learning_switch/lib/learning_switch/application.ex b/examples/learning_switch/lib/learning_switch/application.ex new file mode 100644 index 0000000..fa4dab3 --- /dev/null +++ b/examples/learning_switch/lib/learning_switch/application.ex @@ -0,0 +1,20 @@ +defmodule LearningSwitch.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + # List all child processes to be supervised + children = [ + # Starts a worker by calling: LearningSwitch.Worker.start_link(arg) + # {LearningSwitch.Worker, arg}, + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: LearningSwitch.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/examples/learning_switch/lib/learning_switch/fdb.ex b/examples/learning_switch/lib/learning_switch/fdb.ex new file mode 100644 index 0000000..dcc46e7 --- /dev/null +++ b/examples/learning_switch/lib/learning_switch/fdb.ex @@ -0,0 +1,66 @@ +defmodule LearningSwitch.FDB do + use Agent + + defmodule Entry do + defstruct [ + mac: nil, + port_no: nil, + age_max: 0, + last_update: 0 + ] + + def new(mac, port_no, age_max \\ 180) do + %Entry{ + mac: mac, + port_no: port_no, + age_max: age_max, + last_update: :os.timestamp + } + end + + def update(self, port_no) do + %{self|port_no: port_no, last_update: :os.timestamp} + end + + def aged_out?(self) do + :timer.now_diff(:os.timestamp, self.last_update) > (1000 * self.age_max) + end + end + + def start_link do + Agent.start_link(&Map.new/0) + end + + def lookup(self, mac) do + entry = Agent.get(self, &Map.get(&1, mac)) + entry && entry.port_no + end + + def learn(self, mac, port_no) do + entry = Agent.get(self, &Map.get(&1, mac)) + entry = if entry do + Entry.update(entry, port_no) + else + Entry.new(mac, port_no) + end + Agent.update(self, &Map.put(&1, mac, entry)) + end + + def age(self) do + mac_addrs = Agent.get(self, &find_aged_entries(&1)) + for mac <- mac_addrs do + Agent.update(self, &Map.delete(&1, mac)) + end + end + + # private function + + defp find_aged_entries(map) do + Enum.flat_map(map, fn({mac, entry}) -> + case Entry.aged_out?(entry) do + true -> [mac] + false -> [] + end + end) + end +end diff --git a/examples/learning_switch/lib/learning_switch/ofctl.ex b/examples/learning_switch/lib/learning_switch/ofctl.ex new file mode 100644 index 0000000..db1714c --- /dev/null +++ b/examples/learning_switch/lib/learning_switch/ofctl.ex @@ -0,0 +1,138 @@ +defmodule LearningSwitch.Ofctl do + use GenServer + use Tres.Controller + + import Logger + + alias LearningSwitch.FDB + + @ingress_filtering_table_id 0 + @forwarding_table_id 1 + + @aging_time 180 + + @mcast {"010000000000", "110000000000"} + @bcast "ffffffffffff" + @ipv6_mcast {"333300000000", "ffff00000000"} + + defmodule State do + defstruct [ + datapath_id: nil, + conn_ref: nil, + fdb_pid: nil + ] + end + + def start_link(datapath_id, args) do + GenServer.start_link(__MODULE__, [datapath_id, args]) + end + + def init([datapath_id, _args]) do + :ok = debug("Switch Ready: datapath_id: #{inspect(datapath_id)}") + conn_ref = SwitchRegistry.monitor(datapath_id) + {:ok, pid} = FDB.start_link + init_datapath(datapath_id) + state = %State{ + datapath_id: datapath_id, + conn_ref: conn_ref, + fdb_pid: pid + } + {:ok, state} + end + + def handle_info(%PacketIn{} = packet_in, state) do + <<_dhost::6-bytes, shost::6-bytes, _rest::bytes>> = packet_in.data + eth_src = Openflow.Utils.to_hex_string(shost) + FDB.learn(state.fdb_pid, eth_src, packet_in.in_port) + add_forwarding_flow_and_packet_out(packet_in, state) + :ok = debug("PacketIn: eth_src: #{eth_src} datapath_id: #{inspect(state.datapath_id)}") + {:noreply, state} + end + def handle_info({:'DOWN', ref, :process, _pid, _reason}, %State{conn_ref: ref} = state) do + :ok = debug("Switch Disconnected: datapath_id: #{inspect(state.datapath_id)}") + {:stop, :normal, state} + end + def handle_info(info, state) do + :ok = warn("Unhandled message #{inspect(info)}: #{inspect(state.datapath_id)}") + {:noreply, state} + end + + # private functions + + defp init_datapath(datapath_id) do + init_flow_tables(datapath_id) + end + + defp init_flow_tables(datapath_id) do + for flow_options <- [ + add_default_broadcast_flow_entry(), + add_default_flooding_flow_entry(), + add_multicast_mac_drop_flow_entry(), + add_ipv6_multicast_mac_drop_flow_entry(), + add_default_forwarding_flow_entry()] do + send_flow_mod_add(datapath_id, flow_options) + end + end + + defp add_forwarding_flow_and_packet_out(packet_in, state) do + <> = packet_in.data + eth_dst = Openflow.Utils.to_hex_string(dhost) + port_no = FDB.lookup(state.fdb_pid, eth_dst) + add_forwarding_flow_entry(packet_in, port_no) + packet_out(packet_in, port_no || :flood) + end + + defp packet_out(%PacketIn{datapath_id: datapath_id, data: data}, port_no) do + send_packet_out( + datapath_id, + data: data, + actions: [Output.new(port_no)] + ) + end + + defp add_forwarding_flow_entry(_packet_in, nil), do: :noop + defp add_forwarding_flow_entry(%PacketIn{datapath_id: datapath_id, data: data} = packet_in, port_no) do + <> = data + send_flow_mod_add( + datapath_id, + idle_timeout: @aging_time, + priority: 2, + match: Match.new( + in_port: packet_in.in_port, + eth_dst: Openflow.Utils.to_hex_string(dhost), + eth_src: Openflow.Utils.to_hex_string(shost)), + instructions: [ApplyActions.new(Output.new(port_no))] + ) + end + + defp add_default_broadcast_flow_entry do + [table_id: @forwarding_table_id, + priority: 3, + match: Match.new(eth_dst: @bcast), + instructions: [ApplyActions.new(Output.new(:flood))]] + end + + defp add_default_flooding_flow_entry do + [table_id: @forwarding_table_id, + priority: 1, + instructions: [ApplyActions.new(Output.new(:controller))]] + end + + defp add_multicast_mac_drop_flow_entry do + [table_id: @ingress_filtering_table_id, + priority: 2, + match: Match.new(eth_dst: @mcast)] + end + + defp add_ipv6_multicast_mac_drop_flow_entry do + [table_id: @ingress_filtering_table_id, + priority: 2, + match: Match.new(eth_dst: @ipv6_mcast)] + end + + defp add_default_forwarding_flow_entry do + [table_id: @ingress_filtering_table_id, + priority: 1, + instructions: [GotoTable.new(@forwarding_table_id)]] + end +end diff --git a/examples/learning_switch/mix.exs b/examples/learning_switch/mix.exs new file mode 100644 index 0000000..97e36d2 --- /dev/null +++ b/examples/learning_switch/mix.exs @@ -0,0 +1,23 @@ +defmodule LearningSwitch.Mixfile do + use Mix.Project + + def project do + [ + app: :learning_switch, + version: "0.1.0", + elixir: "~> 1.5", + start_permanent: Mix.env == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [extra_applications: [:logger, :tres], + mod: {LearningSwitch.Application, []}] + end + + defp deps do + [{:tres, path: "../../../tres"}] + end +end diff --git a/examples/learning_switch/mix.lock b/examples/learning_switch/mix.lock new file mode 100644 index 0000000..9380bbc --- /dev/null +++ b/examples/learning_switch/mix.lock @@ -0,0 +1,2 @@ +%{"binpp": {:git, "https://github.com/jtendo/binpp.git", "64bd68d215d1a6cd35871e7c134d7fe2e46214ea", [branch: "master"]}, + "ranch": {:hex, :ranch, "1.4.0", "10272f95da79340fa7e8774ba7930b901713d272905d0012b06ca6d994f8826b", [], [], "hexpm"}} diff --git a/examples/learning_switch/test/learning_switch_test.exs b/examples/learning_switch/test/learning_switch_test.exs new file mode 100644 index 0000000..095aaad --- /dev/null +++ b/examples/learning_switch/test/learning_switch_test.exs @@ -0,0 +1,8 @@ +defmodule LearningSwitchTest do + use ExUnit.Case + doctest LearningSwitch + + test "greets the world" do + assert LearningSwitch.hello() == :world + end +end diff --git a/examples/learning_switch/test/test_helper.exs b/examples/learning_switch/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/examples/learning_switch/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()