5.2 - Simple Definition

A simple header is one that has fields that are at constant length and offset into the header. That is, you don't have to check flags to see if a field is present in the header or calculate offset of a field based on some other fields and conditions. Complex headers are covered in the next section.

Ethernet header is one of those fairly simple definitions that we can use as an example. Its perfect because besides the read/write methods it also contains other useful information that a programmer may want, specifically EtherTypes. EtherTypes is a table of predefined values that ethernet and a host of other protocol definitions use as id for the next header in packet's daisy-chain of headers. This is a useful table, that has no bearing on the header itself, but is useful information that is bundled with the Ethernet header as an enum table enclosed inside Ethernet class. We'll get to that later.

@Header(length=14)
public class Ethernet extends JHeader {

  @Field(offset=0, length=48, description="destination MAC address")
  public byte[] destination() {
    return super.getByteArray(0, 6); // Offset 0, length 6 bytes
  }

  public byte[] destinationToByteArray(byte[] storage) {
    return super.getByteArray(0, storage); // Offset 0, length 6 bytes
  }

  @Field(offset=48, length=48, description="sourceMAC address")
  public byte[] source() {
    return super.getByteArray(6, 6); // Offset 6, length 6 bytes
  }

  @Field(offset=96, length=16) address")
  public int type() {
    return super.getUShort(12); // Offset 12, length 2 bytes
  }
}

Lets go through this example line by line and see what we have there.

Line #1, is a java annotation named @Header and one length=14 parameter. @Header defines this as a header definition of static length of 14 bytes. That is Ethernet header is always 14 bytes long.

Line #2, notice Ethernet is a subclass of JHeader which in turn is a subclass of JBuffer. This is what allows us to use those "super" methods. They are all defined in JBuffer class.

Line #4, @Field annotation that tells the header that this method is used to read values out of a header's field. It provides some information about the field within the header as well. Offset is the exact bit offset from the start of the header where this field starts (you have bit precision here), the length is length of the field in bits as well. The description parameter provides a static description of the field. The description is printed out along side the field when you dump a packet to textual format.

Line #5, is our main field getter method. It returns a byte[] and takes no parameters. The name of the method is also the name of our field. The method's name is actually scanned from the class file itself and used as field name when dumping textual output. The name otherwise is irrelevant for reading data out of the header by a user. Its just a plain old method you use in your program to read that MAC address. Field names are like header IDs. They are unique names that identify a field. There may be other methods within the header all related to a field and the name is the ID that is used to group them all together.

Line #6, one of many getter methods provided by JBuffer base class. This particular method, reads 6 bytes out of the buffer at offset 0 and returns it as a newly created byte[]. JBuffer methods use bytes for offsets and lengths.

Line #9, notice this method is not marked with @Field annotation. This means it won't be included in textual output (ie. JPacket.toString()). It also reads the same MAC address our previous method did. The difference is that it allows the user of this method to supply a byte[] buffer for storing the MAC address instead of allocating a new array. This way, you can supply and reuse the same byte[] for address retrieval. You can put anything inside a header file that is useful to the user. This super.getByteArray() method returns the same byte[] that was passed into it.

Lines 13-16, same as our destination field. The only things that changed are the name of the accessor thus a different field name, offsets and description. Again, @Field offsets and length parameters are in bits. The JBuffer getter methods take bytes.

Line 18 & 19 declares another field, name taken from the method on #19. Offset is updated to 96 bits or 12 bytes into the header and this time the length is 16 bits, an unsigned short. We also use a JBuffer.getUShort(int offset) method which returns an unsigned short (java type int).

Hint: the annotation is java language and its parameters take constants. Therefore you can use expressions such as @Field(offset = 12 * 8, length = 6 * 8) to make it easier to keep track of offsets and lengths in bytes. You will see these simple expressions sprinkled through out the CORE protocol definitions. You can even use predefined constant variables that are public final static.

That is the entire working header definition file. Notice that all the annotations are there only for this textual output. The methods themselves is what gives a regular programmer access to header's structure, the data. The @Field annotations are there to give jNetPcap runtime ability to extract certain things out of the header so it can make pretty output for us such as the one of an actual Ethernet header.

Ethernet:  ******* Ethernet (Eth) offset=0 length=14
Ethernet: 
Ethernet:      destination = 00-E0-F9-CC-18-00
Ethernet:           source = 00-60-08-9F-B1-F3
Ethernet:         protocol = 0x800 (2048) [ip version 4]
Ethernet: 

The output is slightly different than our simple header definition would deliver, the difference is that the production Ethernet header definition sets some extra parameters such as @Header(nicname="Eth").

All this was generated automatically for us from our header definition. Notice the interesting "[ip version 4]" description string that is on the protocol line. This description can't be static, as that field can have many different values there. The description is protocol number specific. That had to be looked up somehow. How does the real Ethernet header definition do the lookup that results in user friendly string that says "ip version 4". Hmm, lets take a look.

Everything we defined in our Ethernet example so far is static. Offsets, descriptions, length, everything. But what if we wanted to include a dynamic description, that changes depending on what's in the protocol field.

The answer is use of runtime description. A static description is stored within the Field's static state, it never changes. But we can replace that static state for descrption with method that will be used to get the description string. That method will be called whenever a description string is needed. It can return whatever we want. The method is also right in the header definition, it has access to the entire header just like any other field accessor method. Therefore it can make decisions and do look ups. It could even go off a look something up on the internet if we wanted it to.

To setup a runtime description for this field we need to add a new method and mark it with @Dynamic annotation.

@Header(length=14)
public class Ethernet extends JHeader {

  @Field(offset=0, length=48, description="destination MAC address")
  public byte[] destination() {
    return super.getByteArray(0, 6); // Offset 0, length 6 bytes
  }

  public byte[] destinationToByteArray(byte[] storage) {
    return super.getByteArray(0, storage); // Offset 0, length 6 bytes
  }

  @Field(offset=48, length=48, description="sourceMAC address")
  public byte[] source() {
    return super.getByteArray(6, 6); // Offset 6, length 6 bytes
  }

  @Field(offset=96, length=16) address")
  public int type() {
    return super.getUShort(12); // Offset 12, length 2 bytes
  }

  @Dynamic(Field.Property.DESCRIPTION)
  public String typeDescription() {
    return (type() == 0x800)?"ip version 4":null;
  }
}

Line #23, @Dynamic annotation marks the method as a runtime method that is associated with a field. The type of function it performs is Field.Property.DESCRIPTION, another words it returns a description of the field as a string. Whenever the runtime wants a description of the field to include in its output it calls this method. The user can too call this method and will be a user friendly description of the field's value, its a normal public method that returns a string.

Line #24, the name of the method is significant in this case. We used our field name "type" and appended to it "Description" to get "typeDescription". The runtime is able to figure out the field name from this as "type". So this description method is associated with type field. @Dynamic(field="type", value=Field.Property.DESCRIPTION) would be another way to do exactly same thing but explicitly, instead of implicitly.

Line #25, what ever we want. In this case we read the type() field, using already existing access and check if it returns 0x800. If it does we return a user friedly string, other null which tells the formatter that there is no description doesn't make any output.

The real Ethernet header definition contains an enum table called EthernetTypes. The description method actually uses that enum table to lookup values and return descriptions. The actual lookup code looks like this:

return EthernetType.toString(this.type()); // Lookup in enum table

This is a normal enum table. Again you can create the tables and other support classes however you like to make the lookup or any other operation you need to do you like. Its up to you.

Other @Dynamic types that you can define. Just keep in mind they each may return a different type, but none of the take a parameter.

  • CHECK - a dynamic method that checks if an optional field is actually avaiable
  • OFFSET - if offset of a field is not constant, this method calculates this offset
  • LENGTH - if length of a field is not constant, this method can calculate that length
  • VALUE - you can even override the method that returns the value of the field
  • DESCRIPTION - dynamic value description
  • MASK - a bit mask used in displaying sub-fields
  • DISPLAY - used to override the name of the field, only for display purposes