diff --git a/lib/openflow/enums.ex b/lib/openflow/enums.ex index ab41448..4d43189 100644 --- a/lib/openflow/enums.ex +++ b/lib/openflow/enums.ex @@ -1047,6 +1047,25 @@ defmodule Openflow.Enums do add: 0, delete: 1, clear: 2 + ], + + table_feature_prop_type: [ + instructions: 0, + instructions_miss: 1, + next_tables: 2, + next_tables_miss: 3, + write_actions: 4, + write_actions_miss: 5, + apply_actions: 6, + apply_actions_miss: 7, + match: 8, + wildcards: 10, + write_setfield: 12, + write_setfield_miss: 13, + apply_setfield: 14, + apply_setfield_miss: 15, + experimenter: 0xfffe, + experimenter_miss: 0xffff ] ] diff --git a/lib/openflow/instructions/experimenter.ex b/lib/openflow/instructions/experimenter.ex index 138b10b..c0c624d 100644 --- a/lib/openflow/instructions/experimenter.ex +++ b/lib/openflow/instructions/experimenter.ex @@ -3,7 +3,7 @@ defmodule Openflow.Instruction.Experimenter do alias __MODULE__ - def new(exp_id, data) do + def new(exp_id, data \\ "") do %Experimenter{exp_id: exp_id, data: data} end diff --git a/lib/openflow/match.ex b/lib/openflow/match.ex index bd40634..0bd54a7 100644 --- a/lib/openflow/match.ex +++ b/lib/openflow/match.ex @@ -33,27 +33,54 @@ defmodule Openflow.Match do end def codec_header(oxm_field) when is_atom(oxm_field) do + oxm_field = case has_mask(oxm_field) do + 1 -> + string = to_string(oxm_field) + "masked_" <> field = string + String.to_atom(field) + 0 -> + oxm_field + end case Openflow.Match.Field.vendor_of(oxm_field) do oxm_class when oxm_class in [:nxm_0, :nxm_1, :openflow_basic, :packet_register] -> oxm_class_int = Openflow.Enums.to_int(oxm_class, :oxm_class) oxm_field_int = Openflow.Enums.to_int(oxm_field, oxm_class) oxm_length = div(Openflow.Match.Field.n_bits_of(oxm_field), 8) - <> - experimenter when experimenter in [:nicira_ext_match, :onf_ext_match] -> + has_mask = has_mask(oxm_field) + <> + experimenter when experimenter in [:nicira_ext_match, :onf_ext_match, :hp_ext_match] -> oxm_class_int = 0xffff experimenter_int = Openflow.Enums.to_int(experimenter, :experimenter_oxm_vendors) oxm_field_int = Openflow.Enums.to_int(oxm_field, experimenter) oxm_length = div(Openflow.Match.Field.n_bits_of(oxm_field) + 4, 8) - <> + has_mask = has_mask(oxm_field) + <> end end - def codec_header(<>) do + def codec_header(<>) do oxm_class = Openflow.Enums.to_atom(oxm_class_int, :oxm_class) - Openflow.Enums.to_atom(oxm_field_int, oxm_class) + case oxm_has_mask do + 0 -> Openflow.Enums.to_atom(oxm_field_int, oxm_class) + 1 -> + field_str = + oxm_field_int + |> Openflow.Enums.to_atom(oxm_class) + |> to_string + String.to_atom("masked_" <> field_str) + end end - def codec_header(<<0xffff::16, oxm_field_int::7, _oxm_has_mask::1, _oxm_length::8, experimenter_int::32>>) do + def codec_header(<<0xffff::16, oxm_field_int::7, oxm_has_mask::1, _oxm_length::8, experimenter_int::32>>) do experimenter = Openflow.Enums.to_atom(experimenter_int, :experimenter_oxm_vendors) Openflow.Enums.to_atom(oxm_field_int, experimenter) + case oxm_has_mask do + 0 -> Openflow.Enums.to_atom(oxm_field_int, experimenter) + 1 -> + field_str = + oxm_field_int + |> Openflow.Enums.to_atom(experimenter) + |> to_string + String.to_atom("masked_" <> field_str) + end end def header_size(<<_oxm_class_int::16, _oxm_field_int::7, _oxm_has_mask::1, _oxm_length::8, _::bytes>>), @@ -151,4 +178,16 @@ defmodule Openflow.Match do match_class = Openflow.Match.Field.vendor_of(field_name) %{class: match_class, field: field_name, has_mask: false, value: value_bin} end + + defp has_mask(oxm_field) when is_atom(oxm_field) do + has_mask? = + oxm_field + |> to_string + |> String.match?(~r/^masked_/) + if has_mask? do + 1 + else + 0 + end + end end diff --git a/lib/openflow/multipart/table_features/body.ex b/lib/openflow/multipart/table_features/body.ex new file mode 100644 index 0000000..c4903fc --- /dev/null +++ b/lib/openflow/multipart/table_features/body.ex @@ -0,0 +1,297 @@ +defmodule Openflow.Multipart.TableFeatures.Body do + defstruct [ + table_id: 0, + name: "", + metadata_match: 0, + metadata_write: 0, + config: [], + max_entries: 0, + instructions: nil, + instructions_miss: nil, + next_tables: nil, + next_tables_miss: nil, + write_actions: nil, + write_actions_miss: nil, + apply_actions: nil, + apply_actions_miss: nil, + match: nil, + wildcards: nil, + write_setfield: nil, + write_setfield_miss: nil, + apply_setfield: nil, + apply_setfield_miss: nil + ] + + alias __MODULE__ + + @max_table_name_len 32 + @prop_header_length 4 + @table_features_length 64 + + @instructions 0 + @instructions_miss 1 + @next_tables 2 + @next_tables_miss 3 + @write_actions 4 + @write_actions_miss 5 + @apply_actions 6 + @apply_actions_miss 7 + @match 8 + @wildcards 10 + @write_setfield 12 + @write_setfield_miss 13 + @apply_setfield 14 + @apply_setfield_miss 15 + + @prop_keys [ + :instructions, + :instructions_miss, + :next_tables, + :next_tables_miss, + :write_actions, + :write_actions_miss, + :apply_actions, + :apply_actions_miss, + :match, + :wildcards, + :write_setfield, + :write_setfield_miss, + :apply_setfield, + :apply_setfield_miss + ] + + def new(options) do + %Body{ + table_id: options[:table_id] || 0, + name: options[:name] || "", + metadata_match: options[:metadata_match] || 0, + metadata_write: options[:metadata_write] || 0, + config: options[:config] || [], + max_entries: options[:max_entries] || 0, + instructions: options[:instructions], + instructions_miss: options[:instructions_miss], + next_tables: options[:next_tables], + next_tables_miss: options[:next_tables_miss], + write_actions: options[:write_actions], + write_actions_miss: options[:write_actions_miss], + apply_actions: options[:apply_actions], + apply_actions_miss: options[:apply_actions_miss], + match: options[:match], + wildcards: options[:wildcards], + write_setfield: options[:write_setfield], + write_setfield_miss: options[:write_setfield_miss], + apply_setfield: options[:apply_setfield], + apply_setfield_miss: options[:apply_setfield_miss] + } + end + + def read(binary) do + do_read([], binary) + end + + def to_binary(features) do + do_to_binary("", features) + end + + # private functions + + defp do_to_binary(acc, []), do: acc + defp do_to_binary(acc, [table|rest]) do + do_to_binary(<>, rest) + end + + defp do_read(acc, ""), do: Enum.reverse(acc) + defp do_read(acc, <> = binary) do + <> = binary + do_read([decode(features_bin)|acc], rest) + end + + defp decode(<<_length::16, table_id::8, _::size(5)-unit(8), + name_bin::size(@max_table_name_len)-bytes, + metadata_match::64, metadata_write::64, + config_int::32, max_entries::32, props_bin::bytes>>) do + name = Openflow.Utils.decode_string(name_bin) + config = Openflow.Enums.int_to_flags(config_int, :table_config) + body = %Body{table_id: table_id, + name: name, + metadata_match: metadata_match, + metadata_write: metadata_write, + config: config, + max_entries: max_entries} + decode_props(body, props_bin) + end + + defp encode(table) do + filter_fn = fn(key) -> not is_nil(Map.get(table, key)) end + keys = Enum.filter(@prop_keys, filter_fn) + props_bin = encode_props("", table, keys) + length = @table_features_length + byte_size(props_bin) + %Body{table_id: table_id, + name: name, + metadata_match: metadata_match, + metadata_write: metadata_write, + config: config, + max_entries: max_entries} = table + config_int = Openflow.Enums.flags_to_int(config, :table_config) + name_bin = Openflow.Utils.encode_string(name, @max_table_name_len) + <> + end + + defp decode_props(body, ""), do: body + defp decode_props(body, <>) + when type_int == @instructions or type_int == @instructions_miss do + pad_length = Openflow.Utils.pad_length(length, 8) + value_length = length - @prop_header_length + <> = tail + instructions = decode_instructions([], instructions_bin) + type = Openflow.Enums.to_atom(type_int, :table_feature_prop_type) + body + |> struct(%{type => instructions}) + |> decode_props(rest) + end + defp decode_props(body, <>) + when type_int == @next_tables or type_int == @next_tables_miss do + pad_length = Openflow.Utils.pad_length(length, 8) + value_length = length - @prop_header_length + <> = tail + next_tables = for <>, do: table_id + type = Openflow.Enums.to_atom(type_int, :table_feature_prop_type) + body + |> struct(%{type => next_tables}) + |> decode_props(rest) + end + defp decode_props(body, <>) + when type_int == @write_actions or + type_int == @write_actions_miss or + type_int == @apply_actions or + type_int == @apply_actions_miss do + pad_length = Openflow.Utils.pad_length(length, 8) + value_length = length - @prop_header_length + <> = tail + actions = decode_actions([], actions_bin) + type = Openflow.Enums.to_atom(type_int, :table_feature_prop_type) + body + |> struct(%{type => actions}) + |> decode_props(rest) + end + defp decode_props(body, <>) + when type_int == @match or + type_int == @wildcards or + type_int == @write_setfield or + type_int == @write_setfield_miss or + type_int == @apply_setfield or + type_int == @apply_setfield_miss do + pad_length = Openflow.Utils.pad_length(length, 8) + value_length = length - @prop_header_length + <> = tail + matches = decode_matches([], matches_bin) + type = Openflow.Enums.to_atom(type_int, :table_feature_prop_type) + body + |> struct(%{type => matches}) + |> decode_props(rest) + end + defp decode_props(body, <<_type_int::16, length::16, tail::bytes>>) do + pad_length = Openflow.Utils.pad_length(length, 8) + value_length = length - @prop_header_length + <<_::size(value_length)-bytes, _::size(pad_length)-unit(8), rest::bytes>> = tail + decode_props(body, rest) + end + + defp encode_props(acc, _table, []), do: acc + defp encode_props(acc, table, [type|rest]) + when type == :instructions or type == :instructions_miss do + type_int = Openflow.Enums.to_int(type, :table_feature_prop_type) + instructions_bin = encode_instructions("", Map.get(table, type)) + length = @prop_header_length + byte_size(instructions_bin) + pad_length = Openflow.Utils.pad_length(length, 8) + body = <> + encode_props(<>, table, rest) + end + defp encode_props(acc, table, [type|rest]) + when type == :next_tables or type == :next_tables_miss do + type_int = Openflow.Enums.to_int(type, :table_feature_prop_type) + next_tables_bin = to_string(Map.get(table, type)) + length = @prop_header_length + byte_size(next_tables_bin) + pad_length = Openflow.Utils.pad_length(length, 8) + body = <> + encode_props(<>, table, rest) + end + defp encode_props(acc, table, [type|rest]) + when (type == :write_actions or + type == :write_actions_miss or + type == :apply_actions or + type == :apply_actions_miss) do + type_int = Openflow.Enums.to_int(type, :table_feature_prop_type) + actions_bin = encode_actions("", Map.get(table, type)) + length = @prop_header_length + byte_size(actions_bin) + pad_length = Openflow.Utils.pad_length(length, 8) + body = <> + encode_props(<>, table, rest) + end + defp encode_props(acc, table, [type|rest]) + when (type == :match or + type == :wildcards or + type == :write_setfield or + type == :write_setfield_miss or + type == :apply_setfield or + type == :apply_setfield_miss) do + type_int = Openflow.Enums.to_int(type, :table_feature_prop_type) + matches_bin = encode_matches("", Map.get(table, type)) + length = @prop_header_length + byte_size(matches_bin) + pad_length = Openflow.Utils.pad_length(length, 8) + body = <> + encode_props(<>, table, rest) + end + + defp decode_instructions(acc, ""), do: Enum.reverse(acc) + defp decode_instructions(acc, <<0xffff::16, _::16, exp_id::32, rest::bytes>>) do + decode_instructions([Openflow.Instruction.Experimenter.new(exp_id)|acc], rest) + end + defp decode_instructions(acc, <>) do + instruction = Openflow.Enums.to_atom(type_int, :instruction_type) + decode_instructions([instruction|acc], rest) + end + + defp encode_instructions(acc, []), do: acc + defp encode_instructions(acc, [%Openflow.Instruction.Experimenter{exp_id: exp_id}|rest]) do + encode_instructions(<>, rest) + end + defp encode_instructions(acc, [type|rest]) do + type_int = Openflow.Enums.to_int(type, :instruction_type) + encode_instructions(<>, rest) + end + + defp decode_actions(acc, ""), do: Enum.reverse(acc) + defp decode_actions(acc, <<0xffff::16, _::16, exp_id::32, rest::bytes>>) do + decode_actions([Openflow.Action.Experimenter.new(exp_id)|acc], rest) + end + defp decode_actions(acc, <>) do + action = Openflow.Enums.to_atom(type_int, :action_type) + decode_actions([action|acc], rest) + end + + defp encode_actions(acc, []), do: acc + defp encode_actions(acc, [%Openflow.Action.Experimenter{exp_id: exp_id}|rest]) do + encode_actions(<>, rest) + end + defp encode_actions(acc, [type|rest]) do + type_int = Openflow.Enums.to_int(type, :action_type) + encode_actions(<>, rest) + end + + defp decode_matches(acc, ""), do: Enum.reverse(acc) + defp decode_matches(acc, binary) do + length = Openflow.Match.header_size(binary) + <> = binary + field = Openflow.Match.codec_header(header_bin) + decode_matches([field|acc], rest) + end + + defp encode_matches(acc, []), do: acc + defp encode_matches(acc, [field|rest]) do + header_bin = Openflow.Match.codec_header(field) + encode_matches(<>, rest) + end +end diff --git a/lib/openflow/multipart/table_features/reply.ex b/lib/openflow/multipart/table_features/reply.ex new file mode 100644 index 0000000..0d3c834 --- /dev/null +++ b/lib/openflow/multipart/table_features/reply.ex @@ -0,0 +1,41 @@ +defmodule Openflow.Multipart.TableFeatures.Reply do + defstruct( + version: 4, + xid: 0, + datapath_id: nil, # virtual field + aux_id: nil, + flags: [], + tables: [] + ) + + alias __MODULE__ + alias Openflow.Multipart.TableFeatures.Body + + def ofp_type, do: 18 + + def new(tables \\ []) do + %Reply{tables: tables} + end + + def read(<>) do + tables = Body.read(tables_bin) + %Reply{tables: tables} + end + + def to_binary(msg) do + header_bin = Openflow.Multipart.Reply.header(msg) + %Reply{tables: tables} = msg + tables_bin = Openflow.Multipart.TableFeatures.Body.to_binary(tables) + <> + end + + def append_body(%Reply{tables: tables} = message, %Reply{flags: [:more], tables: continue}) do + %{message|tables: [continue|tables]} + end + def append_body(%Reply{tables: tables} = message, %Reply{flags: [], tables: continue}) do + new_tables = [continue|tables] + |> Enum.reverse + |> List.flatten + %{message|tables: new_tables} + end +end diff --git a/lib/openflow/multipart/table_features/request.ex b/lib/openflow/multipart/table_features/request.ex new file mode 100644 index 0000000..3259c5d --- /dev/null +++ b/lib/openflow/multipart/table_features/request.ex @@ -0,0 +1,41 @@ +defmodule Openflow.Multipart.TableFeatures.Request do + defstruct( + version: 4, + xid: 0, + datapath_id: nil, # virtual field + aux_id: nil, + flags: [], + tables: [] + ) + + alias __MODULE__ + alias Openflow.Multipart.TableFeatures.Body + + def ofp_type, do: 18 + + def new(tables \\ []) do + %Request{tables: tables} + end + + def read(<>) do + tables = Body.read(tables_bin) + %Request{tables: tables} + end + + def to_binary(msg) do + header_bin = Openflow.Multipart.Request.header(msg) + %Request{tables: tables} = msg + tables_bin = Openflow.Multipart.TableFeatures.Body.to_binary(tables) + <> + end + + def append_body(%Request{tables: tables} = message, %Request{flags: [:more], tables: continue}) do + %{message|tables: [continue|tables]} + end + def append_body(%Request{tables: tables} = message, %Request{flags: [], tables: continue}) do + new_tables = [continue|tables] + |> Enum.reverse + |> List.flatten + %{message|tables: new_tables} + end +end diff --git a/test/flay.ex b/test/flay.ex index e37c239..f166928 100644 --- a/test/flay.ex +++ b/test/flay.ex @@ -9,7 +9,8 @@ defmodule Flay do datapath_id: nil, tester_pid: nil, conn_ref: nil, - reply_to: nil + reply_to: nil, + default_profile: nil, ] end @@ -19,6 +20,8 @@ defmodule Flay do def init(args) do state = init_controller(args) + TableFeatures.Request.new + |> send_message(state.datapath_id) init_bridge(state.datapath_id) {:ok, state} end @@ -48,6 +51,10 @@ defmodule Flay do send_flow_mod_delete(state.datapath_id, match: Match.new) {:noreply, state} end + def handle_cast(:restore_flow_profile, state) do + send_message(state.default_profile, state.datapath_id) + {:noreply, state} + end def handle_info(%ErrorMsg{} = error, state) do send(state.tester_pid, error) @@ -57,6 +64,9 @@ defmodule Flay do send(state.tester_pid, pktin) {:noreply, state} end + def handle_info(%TableFeatures.Reply{} = table, state) do + {:noreply, %{state|default_profile: table}} + end def handle_info(%PortDesc.Reply{} = desc, state) do GenServer.reply(state.reply_to, desc) {:noreply, %{state|reply_to: nil}} @@ -70,8 +80,8 @@ defmodule Flay do {:noreply, %{state|reply_to: nil}} end # `Catch all` function is required. - def handle_info(_info, state) do - # :ok = warn("[#{__MODULE__}] unhandled message #{inspect(info)}") + def handle_info(info, state) do + :ok = warn("[#{__MODULE__}] unhandled message #{inspect(info)}") {:noreply, state} end @@ -119,6 +129,54 @@ defmodule Flay do end defp init_bridge(datapath_id) do + tables = [ + TableFeatures.Body.new( + table_id: 0, + max_entries: 2000, + instructions: [ + Openflow.Instruction.ApplyActions, + Openflow.Instruction.GotoTable + ], + next_tables: [1], + apply_actions: [ + Openflow.Action.Output, + Openflow.Action.PushVlan, + Openflow.Action.PopVlan, + Openflow.Action.SetField + ], + match: [ + :in_port, :eth_src, :eth_dst, :eth_type, :vlan_vid, + :ip_proto, :ipv4_src, :ipv4_dst, :tcp_dst,:udp_dst + ], + apply_setfield: [ + :eth_src, :eth_dst, :vlan_vid + ] + ), + TableFeatures.Body.new( + table_id: 0, + max_entries: 2000, + instructions: [ + Openflow.Instruction.ApplyActions + ], + next_tables: [], + apply_actions: [ + Openflow.Action.Output, + Openflow.Action.PushVlan, + Openflow.Action.PopVlan, + Openflow.Action.SetField + ], + match: [ + :in_port, :eth_src, :eth_dst, :eth_type, :vlan_vid, + :ip_proto, :ipv4_src, :ipv4_dst, :tcp_dst,:udp_dst + ], + apply_setfield: [ + :eth_src, :eth_dst, :vlan_vid, :ipv4_src, :ipv4_dst, + :arp_spa, :arp_tpa, :arp_tha + ] + ), + ] + TableFeatures.Request.new(tables) + |> send_message(datapath_id) send_flow_mod_delete(datapath_id, table_id: :all) end end diff --git a/test/flog_test.exs b/test/flog_test.exs index 096a226..656b184 100644 --- a/test/flog_test.exs +++ b/test/flog_test.exs @@ -35,7 +35,7 @@ defmodule FlogTest do Code.load_file("test/pf.ex") # GIVEN - setup do + setup_all do setup_applications() wait_for_connected() ports = get_ports_desc() @@ -53,8 +53,7 @@ defmodule FlogTest do ] on_exit fn -> - print_flows() - GenServer.cast(Flay, :flow_del) + GenServer.cast(Flay, :restore_flow_profile) end {:ok, options} end