Create Custom Protocol - lemontree55/packetgen GitHub Wiki

PacketGen allows you adding your own header classes.

Quick start

To add a new/custom header, you first have to define the new header class. For example:

module MyModule
  class MyHeader < PacketGen::Header::Base
    define_attr :field1, BinStruct::Int32
    define_attr :field2, BinStruct::Int32

    # Mandatory to be detected as MyModule::MyHeader,
    # else it will be detected as MyHeader.
    def self.protocol_name; "MyModule::MyHeader"; end
  end
end

Then, class must be declared to PacketGen:

PacketGen::Header.add_class MyModule::MyHeader

Finally, bindings must be declared:

# bind MyHeader as IP protocol number 254 (needed by Packet#parse and Packet#add)
PacketGen::Header::IP.bind MyModule::MyHeader, protocol: 254

And use it:

pkt = Packet.gen('IP').add('MyHeader', field1: 0x12345678)
pkt.myheader.field2.from_human(0x01)
pkt.myheader.to_s # => "\x12\x34\x56\x78\x00\x00\x00\x01"

Add a new header type, in more detail

Define a header class

A new header class should inherit from PacketGen::Header::Base class (or from PacketGen::Header::ASN1Base but this is off topic). This base class implements minimal API (see below) to parse a header from binary string, or to generate binary string from a header class.

PacketGen::Header::Base inherits from BinStruct::Struct, which is a class to define headers or anything else with a binary format containing multiple fields.

Here, magical method is define_attr. This method defines a BinStruct::Struct attribute and its name. You may set as attributes as you need. A type may be a predefined type from BinStruct, or any other BinStruct::Sruct subclass:

class CustomAttribute < BinStruct::Strut
  # add a 16-bit integer attribute
  define_attr :one, BinStruct::Int16
  # add another one
  define_attr :two, BinStruct::Int16
end

class ExampleHeader < PacketGen::Header::Base
  # define a first 32-bit integer attribute
  define_attr :first, BinStruct::Int32
  # add a custom attribute
  define_attr :custom, CustomAttribute
  # Define an integer attribute, composed of bit attributes:
  #  * ack and error are boolean flags, as no size is specified (default to 1)
  #  * rsv is a 4-bit field
  #  * type is a 2-bit field
  define_bit_attr :int8, ack: 1, error: 1, :rsv, 4, :type, 2
end

Here are some example to access these fields:

example = ExampleHeader.new

example.first        #=> Integer
example.first = 0x12345678

example.custom       #=> CustomField
example.custom.one   #=> Integer

example.ack?         #=> Boolean
example.ack          #=> Integer
# May be set as a boolean or as an integer
example.ack = true
example.ack = 1

example.type         #=> Integer
example.type = 1

BinStruct Types

To create new Struct or Header classes, BinStruct provides some base types:

  • unsigned integers:
    • Int8,
    • big-endian integers: Int16 or Int16be, Int32 or Int32be and Int64 or Int64be,
    • little-endian integers: Int16le, Int32le and Int64le,
  • signed integers:
    • SInt8,
    • big-endian integers: SInt16 or SInt16be, SInt32 or SInt32be and SInt64 or SInt64be,
    • little-endian integers: SInt16le, SInt32le and SInt64le,
  • enumerated integers: Int8Enum, Int16Enum, Int16beEnum, Int16leEnum, Int32Enum, Int32beEnum and Int32leEnum,
  • strings: String, IntString (a string with a length field at the beginning) and CString (null-terminated string),
  • arrays: Array to define set of attributes,
  • AbstractTLV to define fields as set of type-length-value,
  • OUI (Organizationally Unique Identifier).

All these types are defined under BinStruct namespace.

Add some methods to a header class

By default, all attributes will have accessors with the good type. By example, a BinStruct::Int32 field may be accessed as an Integer, a BinStruct::String may be accessed as a String, etc.

But, for some reason, you may need to add another accessor, or a method to compute some protocol data.

To do that, you have to understand Struct model. An attribute may be accessed through its accessor, as already seen. But it may also be accessed through Struct's hash.

Struct class defines #[] method to access to attribute object by its name. For example, with our previously defined ExampleHeader:

example.first     #=> Integer
example[:first]   #=> BinStruct::Int32

Sometimes, you will need to access real attribute objects. All attribute objects have common methods:

  • #read reads a binary string to set object,
  • #to_s gives binary string from object,
  • #sz gives binary size,
  • #from_human to set object from a human readable object (often a String or an Integer).

As an example of method to a header class, we will define one to calculate first attribute, which value should be size of custom attribute:

class ExampleHeader
  # compute first attribute
  def calc_first
    self[:first].from_human(self[:custom].sz)
  end
end

Add a new ASN.1 header

Some header may be defined using ASN.1 notation, like SNMP.

To define such a header, use PacketGen::Header::ASN1Base as base class:

class SNMP < PacketGen::Header::ASN1Base
  sequence :message,
            content: [enumerated(:version, value: 'v2c',
                                 enum: { 'v1' => 0, 'v2c' => 1, 'v2' => 2, 'v3' =>}),
                      octet_string(:community, value: 'public'),
                      model(:data, PDUClass)]

  define_attributes :version, :community
end

This definition uses lots of stuffs from rasn1 gem:

  • sequence defines a ASN.1 Sequence named message. This sequence contains:
    • an Enumerated named version,
    • an Octet String named community,
    • and a PDUClass (subclass of RASN1::Model) named data and not defined here.

define_attributes is a helper method to declare attributes from some ASN.1 fields. This helps to mimic standard header behaviour.

Packet magics

PacketGen::Packet defines some magics using specific header methods.

If your header class has a checksum attribute and/or a length attribute, Packet provides magic for them. You have to define a #calc_checksum and/or a calc_length which appropriatly set checksum and/or length attributes respectively. Then Packet#calc_checksum and Packet#calc_length will calculate all checksum and length attributes in all headers, including yours.

If your header class is not an application layer one, you should define a body field of type BinStruct::String. This will allow Packet#parse to automagically parse headers embedded in yours. Same magic will happen for Packet#to_s, Packet#encapsulate, Packet#decapsulate and Packet#add.

If, to respond to a message, some attributes may be inverted in your header, you should define #reply! to help Packet#reply and Packet#reply! do the thing.

Header minimal API

PacketGen::Header::Base and PacketGen::Header::ASN1Base are provided to simplify writing of new headers. But they may not be so useful for some protocol types.

So, here is minimal API needed by PacketGen to handle a header class.

A header MUST have accessors:

  • #packet: get/set packet to which header belongs.

A header MUST respond to:

  • .protocol_name: get protocol name, usually class name as a String, without module path,
  • #method_name: get method name, usually same as protocol name but downcase. This name is used as accessor from packet to access header object,
  • #read: method to parse binary string and decode header,
  • #parse?: return true if decoded header is correct. Used when guessing if header may be decoded from binary string. An example of use is checking first 4-bit attribute for IP version in a IP/IPv6 header.