diff --git a/features/arp_reply.feature b/features/arp_reply.feature index 49a3d430..3e3c8b57 100644 --- a/features/arp_reply.feature +++ b/features/arp_reply.feature @@ -50,5 +50,5 @@ Feature: Arp::Reply 0xc0, 0xa8, 0x53, 0xfe, # sender_protocol_address 0x00, 0x26, 0x82, 0xeb, 0xea, 0xd1, # target_hardware_address 0xc0, 0xa8, 0x53, 0x03, # target_protocol_address - ].pack('C*') + ].pack('C42') """ diff --git a/features/arp_request.feature b/features/arp_request.feature index 0a6aebfa..89555b94 100644 --- a/features/arp_request.feature +++ b/features/arp_request.feature @@ -48,5 +48,5 @@ Feature: Arp::Request 0xc0, 0xa8, 0x53, 0x03, # sender_protocol_address 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # target_hardware_address 0xc0, 0xa8, 0x53, 0xfe, # target_protocol_address - ].pack('C*') + ].pack('C42') """ diff --git a/features/ethernet_header.feature b/features/ethernet_header.feature new file mode 100644 index 00000000..d67faf05 --- /dev/null +++ b/features/ethernet_header.feature @@ -0,0 +1,149 @@ +Feature: EthernetHeader + Scenario: create an Ethernet header + When I create a packet with: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::IPV4 + ) + """ + Then the packet has the following fields and values: + | field | value | + | class | Pio::EthernetHeader | + | destination_mac | ff:ff:ff:ff:ff:ff | + | source_mac | 00:26:82:eb:ea:d1 | + | ether_type.to_hex | 0x08, 0x00 | + + Scenario: create a VLAN-tagged Ethernet header + When I create a packet with: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::VLAN, + vlan_pcp: 5, + vlan_cfi: 0, + vlan_vid: 100 + ) + """ + Then the packet has the following fields and values: + | field | value | + | class | Pio::EthernetHeader | + | destination_mac | ff:ff:ff:ff:ff:ff | + | source_mac | 00:26:82:eb:ea:d1 | + | ether_type.to_hex | 0x81, 0x00 | + | vlan_pcp | 5 | + | vlan_cfi | 0 | + | vlan_vid | 100 | + + Scenario: read an Ethernet header + Given I use the fixture "ethernet_header" + When I create a packet with: + """ruby + Pio::EthernetHeader.read(eval(IO.read('ethernet_header.rb'))) + """ + Then the packet has the following fields and values: + | field | value | + | class | Pio::EthernetHeader | + | destination_mac | ff:ff:ff:ff:ff:ff | + | source_mac | 00:26:82:eb:ea:d1 | + | ether_type.to_hex | 0x08, 0x00 | + + Scenario: read a VLAN-tagged Ethernet header + Given I use the fixture "ethernet_header" + When I create a packet with: + """ruby + Pio::EthernetHeader.read(eval(IO.read('vlan_ethernet_header.rb'))) + """ + Then the packet has the following fields and values: + | field | value | + | class | Pio::EthernetHeader | + | destination_mac | ff:ff:ff:ff:ff:ff | + | source_mac | 00:26:82:eb:ea:d1 | + | ether_type.to_hex | 0x81, 0x00 | + | vlan_pcp | 5 | + | vlan_cfi | 0 | + | vlan_vid | 100 | + + Scenario: convert Ethernet header to Ruby code + When I eval the following Ruby code: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::IPV4 + ).to_ruby + """ + Then the result of eval should be: + """ruby + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, # destination_mac + 0x00, 0x26, 0x82, 0xeb, 0xea, 0xd1, # source_mac + 0x08, 0x00, # ether_type + ].pack('C14') + """ + + Scenario: convert VLAN-tagged Ethernet header to Ruby code + When I eval the following Ruby code: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::VLAN, + vlan_pcp: 5, + vlan_cfi: 0, + vlan_vid: 100 + ).to_ruby + """ + Then the result of eval should be: + """ruby + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, # destination_mac + 0x00, 0x26, 0x82, 0xeb, 0xea, 0xd1, # source_mac + 0x81, 0x00, # ether_type + 0b101_0_000001100100, # vlan_pcp, vlan_cfi, vlan_vid + 0x81, 0x00, # ether_type_vlan + ].pack('C14nC2') + """ + + Scenario: EthernetHeader instance inspection + When I eval the following Ruby code: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::IPV4, + ).inspect + """ + Then the result of eval should be: + """ + # + """ + + Scenario: VLAN-tagged EthernetHeader instance inspection + When I eval the following Ruby code: + """ruby + Pio::EthernetHeader.new( + destination_mac: 'ff:ff:ff:ff:ff:ff', + source_mac: '00:26:82:eb:ea:d1', + ether_type: Pio::Ethernet::Type::VLAN, + vlan_pcp: 5, + vlan_cfi: 0, + vlan_vid: 100 + ).inspect + """ + Then the result of eval should be: + """ + # + """ + + Scenario: EthernetHeader class inspection + When I eval the following Ruby code: + """ruby + Pio::EthernetHeader.inspect + """ + Then the result of eval should be: + """ + EthernetHeader(destination_mac: mac_address, source_mac: mac_address, ether_type: uint16, vlan_pcp: bit3, vlan_cfi: bit1, vlan_vid: bit12) + """ diff --git a/fixtures/ethernet_header/ethernet_header.rb b/fixtures/ethernet_header/ethernet_header.rb new file mode 100644 index 00000000..739268c9 --- /dev/null +++ b/fixtures/ethernet_header/ethernet_header.rb @@ -0,0 +1,5 @@ +[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, # destination_mac + 0x00, 0x26, 0x82, 0xeb, 0xea, 0xd1, # source_mac + 0x08, 0x00, # ether_type +].pack('C*') diff --git a/fixtures/ethernet_header/vlan_ethernet_header.rb b/fixtures/ethernet_header/vlan_ethernet_header.rb new file mode 100644 index 00000000..0de1b379 --- /dev/null +++ b/fixtures/ethernet_header/vlan_ethernet_header.rb @@ -0,0 +1,7 @@ +[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, # destination_mac + 0x00, 0x26, 0x82, 0xeb, 0xea, 0xd1, # source_mac + 0x81, 0x00, # ether_type + 0b101_0_000001100100, # vlan_pcp, vlan_cfi, vlan_vid + 0x81, 0x00, # ether_type_vlan +].pack('C14nC2') diff --git a/lib/pio/arp/format.rb b/lib/pio/arp/format.rb index 0911231f..fac68a78 100644 --- a/lib/pio/arp/format.rb +++ b/lib/pio/arp/format.rb @@ -8,11 +8,11 @@ module Pio class Arp # ARP parser. class Format < BinData::Record - include EthernetHeader + include Ethernet endian :big - ethernet_header ether_type: EtherType::ARP + ethernet_header ether_type: Ethernet::Type::ARP uint16 :hardware_type, value: 1 uint16 :protocol_type, value: 0x0800 uint8 :hardware_length, value: 6 @@ -33,7 +33,7 @@ def to_exact_match(in_port) in_port: in_port, source_mac_address: source_mac, destination_mac_address: destination_mac, - vlan_vid: vlan_vid, + vlan_vid: 0xffff, vlan_priority: vlan_pcp, ether_type: ether_type, tos: 0, diff --git a/lib/pio/arp/message.rb b/lib/pio/arp/message.rb index 6c837652..0128cf2f 100644 --- a/lib/pio/arp/message.rb +++ b/lib/pio/arp/message.rb @@ -1,9 +1,11 @@ require 'pio/arp/format' +require 'pio/ruby_dumper' module Pio class Arp # Base class of ARP Request and Reply class Message + include RubyDumper private_class_method :new def initialize(user_options) @@ -14,15 +16,6 @@ def initialize(user_options) def method_missing(method, *args) @format.__send__ method, *args end - - # Returns a Ruby code representation of this packet, such that - # it can be eval'ed and sent later. - def to_ruby - hexes = @format.field_names.map do |each| - ' ' + __send__(each).to_hex + ", # #{each}" if __send__("#{each}?") - end.compact - ['[', *hexes, "].pack('C*')"].join("\n") - end end end end diff --git a/lib/pio/dhcp/frame.rb b/lib/pio/dhcp/frame.rb index 1241179c..7a8eb705 100644 --- a/lib/pio/dhcp/frame.rb +++ b/lib/pio/dhcp/frame.rb @@ -12,12 +12,12 @@ class Frame < BinData::Record OPTION_FIELD_LENGTH = 60 - include EthernetHeader + include Ethernet include IPv4Header include UdpHeader endian :big - ethernet_header ether_type: EtherType::IPV4 + ethernet_header ether_type: Ethernet::Type::IPV4 ipv4_header ip_protocol: ProtocolNumber::UDP udp_header dhcp_field :dhcp diff --git a/lib/pio/ethernet_header.rb b/lib/pio/ethernet_header.rb index 90b0a3f2..9b242acf 100644 --- a/lib/pio/ethernet_header.rb +++ b/lib/pio/ethernet_header.rb @@ -1,58 +1,66 @@ +require 'pio/ruby_dumper' require 'pio/type/mac_address' module Pio # Adds ethernet_header macro. - module EthernetHeader - # EtherType constants for ethernet_header.ether_type. - module EtherType + module Ethernet + # EtherType constants + module Type ARP = 0x0806 IPV4 = 0x0800 VLAN = 0x8100 LLDP = 0x88cc end - # Ethernet header parser - class Parser < BinData::Record - endian :big - - mac_address :destination_mac - mac_address :source_mac - uint16 :ether_type - bit3 :vlan_pcp_internal, onlyif: :vlan? - bit1 :vlan_cfi, onlyif: :vlan? - bit12 :vlan_vid_internal, onlyif: :vlan? - uint16 :ether_type_vlan, value: :ether_type, onlyif: :vlan? - end - # This method smells of :reek:TooManyStatements + # rubocop:disable MethodLength def self.included(klass) - def klass.ethernet_header(options) + def klass.ethernet_header(options = nil) mac_address :destination_mac mac_address :source_mac - uint16 :ether_type, value: options.fetch(:ether_type) - bit3 :vlan_pcp_internal, onlyif: :vlan? + if options + uint16 :ether_type, value: options.fetch(:ether_type) + else + uint16 :ether_type + end + bit3 :vlan_pcp, onlyif: :vlan? bit1 :vlan_cfi, onlyif: :vlan? - bit12 :vlan_vid_internal, onlyif: :vlan? + bit12 :vlan_vid, onlyif: :vlan? uint16 :ether_type_vlan, value: :ether_type, onlyif: :vlan? end end + # rubocop:enable MethodLength - def ethernet_header - Parser.new(destination_mac: destination_mac, - source_mac: source_mac, - ether_type: ether_type) - end + private - def vlan_vid - vlan? ? vlan_vid_internal : 0xffff + def vlan? + ether_type == Type::VLAN end + end - def vlan_pcp - vlan? ? vlan_pcp_internal : 0 + # Ethernet header generator/parser + class EthernetHeader < BinData::Record + include Ethernet + include RubyDumper + + endian :big + + ethernet_header + + # rubocop:disable LineLength + def self.inspect + 'EthernetHeader(destination_mac: mac_address, source_mac: mac_address, ether_type: uint16, vlan_pcp: bit3, vlan_cfi: bit1, vlan_vid: bit12)' end + # rubocop:enable LineLength - def vlan? - ether_type == EtherType::VLAN + # rubocop:disable LineLength + def inspect + if vlan? + %(#) + else + %(#) + end end + # rubocop:enable LineLength end end diff --git a/lib/pio/icmp/format.rb b/lib/pio/icmp/format.rb index f6143150..a07bdbfc 100644 --- a/lib/pio/icmp/format.rb +++ b/lib/pio/icmp/format.rb @@ -9,12 +9,12 @@ class Icmp class Format < BinData::Record MINIMUM_IP_PACKET_LENGTH = 50 - include EthernetHeader + include Ethernet include IPv4Header endian :big - ethernet_header ether_type: EtherType::IPV4 + ethernet_header ether_type: Ethernet::Type::IPV4 ipv4_header ip_protocol: ProtocolNumber::ICMP uint8 :icmp_type uint8 :icmp_code, initial_value: 0 diff --git a/lib/pio/ipv4_header.rb b/lib/pio/ipv4_header.rb index 2e9e786e..7e89e0c4 100644 --- a/lib/pio/ipv4_header.rb +++ b/lib/pio/ipv4_header.rb @@ -39,7 +39,7 @@ def to_exact_match(in_port) in_port: in_port, source_mac_address: source_mac, destination_mac_address: destination_mac, - vlan_vid: vlan_vid, + vlan_vid: vlan? ? vlan_vid : 0xffff, vlan_priority: vlan_pcp, ether_type: ether_type, tos: ip_type_of_service, diff --git a/lib/pio/lldp/frame.rb b/lib/pio/lldp/frame.rb index 8038a916..7909c8c7 100644 --- a/lib/pio/lldp/frame.rb +++ b/lib/pio/lldp/frame.rb @@ -11,11 +11,11 @@ module Pio class Lldp # LLDP frame class Frame < BinData::Record - include EthernetHeader + include Ethernet endian :big - ethernet_header ether_type: EtherType::LLDP + ethernet_header ether_type: Ethernet::Type::LLDP chassis_id_tlv :chassis_id port_id_tlv :port_id ttl_tlv :ttl, initial_value: 120 diff --git a/lib/pio/parser.rb b/lib/pio/parser.rb index fe16e0e3..605d1f67 100644 --- a/lib/pio/parser.rb +++ b/lib/pio/parser.rb @@ -16,12 +16,12 @@ class EthernetFrame < BinData::Record # IPv4 packet parser class IPv4Packet < BinData::Record - include EthernetHeader + include Ethernet include IPv4Header endian :big - ethernet_header ether_type: EtherType::IPV4 + ethernet_header ether_type: Ethernet::Type::IPV4 ipv4_header uint16 :transport_source_port @@ -33,11 +33,11 @@ class IPv4Packet < BinData::Record def self.read(raw_data) ethernet_frame = EthernetFrame.read(raw_data) case ethernet_frame.ether_type - when EthernetHeader::EtherType::IPV4, EthernetHeader::EtherType::VLAN + when Ethernet::Type::IPV4, Ethernet::Type::VLAN IPv4Packet.read raw_data - when EthernetHeader::EtherType::ARP + when Ethernet::Type::ARP Pio::Arp.read raw_data - when EthernetHeader::EtherType::LLDP + when Ethernet::Type::LLDP Pio::Lldp.read raw_data else ethernet_frame diff --git a/lib/pio/ruby_dumper.rb b/lib/pio/ruby_dumper.rb new file mode 100644 index 00000000..b9c3500d --- /dev/null +++ b/lib/pio/ruby_dumper.rb @@ -0,0 +1,59 @@ +module Pio + # defines to_ruby method + module RubyDumper + # Returns a Ruby code representation of this packet, such that + # it can be eval'ed and sent later. + # + # rubocop:disable AbcSize + # rubocop:disable MethodLength + # rubocop:disable CyclomaticComplexity + # rubocop:disable PerceivedComplexity + def to_ruby + pack_template = '' + bytes = '' + bit = false + bit_names = [] + field_names.each do |each| + next unless __send__("#{each}?") + if /Bit(\d+)$/ =~ __send__(each).class.to_s + bit_length = Regexp.last_match(1) + if bit + bit_names << each + bytes << format("_%0#{bit_length}b", __send__(each)) + else + bit_names = [each] + bytes << format(" 0b%0#{bit_length}b", __send__(each)) + end + bit = true + else + if bit + bytes << ", # #{bit_names.join(', ')}\n" + pack_template << 'n' + bit_names = [] + bit = false + end + list = __send__(each).to_hex + bytes << " #{list}, # #{each}\n" + pack_template << 'C' * (list.count(',') + 1) + end + end.compact + + template = '' + until pack_template.empty? + if /^(.)(\1+)/ =~ pack_template + pack_template.sub!(/^(.)(\1+)/, '') + template << + "#{Regexp.last_match(1)}#{Regexp.last_match(2).length + 1}" + else + pack_template.sub!(/^(.)/, '') + template << Regexp.last_match(1) + end + end + "[\n#{bytes}].pack('#{template}')" + end + # rubocop:enable AbcSize + # rubocop:enable MethodLength + # rubocop:enable CyclomaticComplexity + # rubocop:enable PerceivedComplexity + end +end diff --git a/lib/pio/udp.rb b/lib/pio/udp.rb index 30cc0dd6..117dedb0 100644 --- a/lib/pio/udp.rb +++ b/lib/pio/udp.rb @@ -6,12 +6,12 @@ module Pio # UDP packet format class Udp < BinData::Record - include EthernetHeader + include Ethernet include IPv4Header include UdpHeader endian :big - ethernet_header ether_type: EtherType::IPV4 + ethernet_header ether_type: Ethernet::Type::IPV4 ipv4_header ip_protocol: ProtocolNumber::UDP udp_header rest :udp_payload