Writing a Packet Template (dynamic layouts)

Here we describe a new and a more powerful approach to desigining a packet template using dynamic layouts. This tutorial can be considered a continuation to the previous one (based on on static C-lke struct-s). Please note that this new dynamic layout approach complements rather than replaces the old one. All the methods and facilities described in the previous tutorial are still valid and can be used – either by themselves or within this new dynamic layout paradigm.

Why Dynamic Layouts?

For most real-world protocols, the structure of a packet is not statically defined once and for all. Instead, for each particular packet, its structure depends on certain packet data fields. For example, if for an Ethernet frame, the EtherType field tells us what follows next – is it IPv4? or IPv6? ARP? and so on. OK, let’s say, it’s the good old IPv4. Now, we have to check the Protocol field of the IPv4 header to determine the semantics of the follow-up data (ICMP? TCP? UDP? etc.). And so on and so forth, we’ll have to analyze the rest of the packet, layer-by-layer, to discover its complete structure.

For descibing such complex data structures, simple C-like struct-s won’t be enough – we need to somehow encode all that decision-making process that happens at runtime and depends on actual bytes of the packet.

Luckily enough, Jancy provides a dedicated facility just for that, and this facility is called dynamic layouts. Using dynamic layouts, the programmer can conveniently mix declarative description via struct-s together with imperative code performing all those runtime checks and branching.

But enough chit-chat, let’s get to writing a packet template for the protocol TutoProto!

Layout Function

Dynamic packet templates are defined using a so-called layout function. This function receives a pointer to a jnc.DynamicLayout object and populates it using dyfield (i.e., dynamic field) declarations.

Within this function, you can access to any previously declared dyfield normally, just like you do with local variables. And, of course, you can implement any logic using the if/else/switch branching and/or loops, thus building structures of arbitrary complexity.

void layoutMyPacket(jnc.DynamicLayout* layout) {
    dylayout (layout) {
        dyfield TutoProtoHdr hdr;

        if (hdr.m_flags & TutoProtoFlags.Error)
            dyfield TutoProtoError error;
        else
            switch (hdr.m_command) {
            case TutoProtoCommand.GetVersion:
                if (hdr.m_flags & TutoProtoFlags.Reply)
                    dyfield TutoProtoVersion version;
                break;

            case TutoProtoCommand.Read:
                if (hdr.m_flags & TutoProtoFlags.Reply)
                    dyfield char data[hdr.m_size - sizeof(TutoProtoHdr)];
                else
                    dyfield TutoProtoRange read;
                break;

            case TutoProtoCommand.Write:
                if (hdr.m_flags & TutoProtoFlags.Reply)
                    break; // reply to TutoProtoCommand.Write is empty

                dyfield TutoProtoRange write;
                dyfield char data[write.m_length & 0xffff]; // limit max size
                break;
            }

        dyfield bigendian uint16_t crc;
    }
}

To add this function as a packet template, add the packetTemplate attribute. For dynamic packet templates, it also could be helpful sense to add the fixedSize attribute – this way, the hex editor will not allow you to change the number of bytes defined by the template.

[
    packetTemplate,
    fixedSize
]
void layoutMyPacket(jnc.DynamicLayout* layout) {
    ...
}

User Actions in Layout Functions

Simple struct-based templates can have userAction methods to programmatically calculate field values (checksums, lengths, encode payloads, etc). In order to do the same in a layout function, you must wrap such methods into an empty structure and then add this structure to the layout using a dyfield declaratoin. The user action methods can then traverse the packet bytes using address arithmetics and update fields as necessary.

struct UpdateCrc {
    [
        userAction = "Calculate CRC",
        autorun = "Auto-update CRC"
    ]
    void calcCrc() {
        utf16_t* crc = (utf16_t*)this - 1; // get the location of CRC
        size_t size = dynamic offsetof(crc); // calculate packet size
        *crc = crc16_ansi((char*)crc - size, size); // update CRC
    }
}

[
    packetTemplate,
    fixedSize
]
void layoutMyPacket(jnc.DynamicLayout* layout) {
    ...
    dyfield uint16_t crc;
    dyfield UpdateCrc updateCrc; // <- add the calcCrc() action
}

Visual Tweaks

At this point, we have a fully functional packet template. Now, let’s improve its visual appearance. This could be done by adding attibutes to corresponding dyfield declarations. The summary of relevant attributes is as follows:

Attribute Description
ungroup Don’t create a subgroup for each struct field – i.e., add fields at the same level
binary Disable the property grid editor for a field – i.e., you could only edit bytes of this field using the hex-editor. This could make sense for binary blobs.
displayName Specify the visible name for a field
formatSpec Specify the printf format specifier for a field (e.g., 0x%04X to display it as a hexadecimal integer)
backColor Specify the background color for a field. Applies both to the property grid and the hex-editor.

The resulting packet template will look something like this:

_images/packet-template-dylayout.png
Complete source code
import "crc16.jnc"
import "ui_Color.jnc"

struct TutoProtoHdr {
    [
        displayName = "STX",
        formatSpec = "0x%02X"
    ]
    uint8_t m_stx;

    [
        displayName = "Command",
        displayType = typeof(TutoProtoCommand)
    ]
    uint8_t m_command : 4;

    [
        displayName = "Flags",
        displayType = typeof(TutoProtoFlags)
    ]
    uint8_t m_flags   : 4;

    [
        displayName = "ID",
        formatSpec = "#%d"
    ]
    bigendian uint16_t m_id;

    [ displayName = "Size" ]
    bigendian uint16_t m_size;

    [
        userAction = "Calculate size",
        autorun = "Auto-update size"
    ]
    void calcSize() {
        m_size = dynamic sizeof(this); // calculate packet size
        m_stx = 0x02;
    }
}

enum TutoProtoCommand: uint8_t {
    GetVersion,
    Read,
    Write,
}

bitflag enum TutoProtoFlags: uint8_t {
    Reply,
    Error,
}

struct TutoProtoVersion {
    [ displayName = "Major" ]
    uint8_t m_major;

    [ displayName = "Minor" ]
    uint8_t m_minor;

    [ displayName = "Patch" ]
    uint8_t m_patch;
}

struct TutoProtoRange {
    [
        displayName = "Offset",
        formatSpec = "0x%04X"
    ]
    bigendian uint32_t m_offset;

    [ displayName = "Length" ]
    bigendian uint32_t m_length;
}

struct TutoProtoError {
    [ displayName = "Error" ]
    bigendian uint32_t m_error;
}

struct UpdateCrc {
    [
        userAction = "Calculate CRC",
        autorun = "Auto-update CRC"
    ]
    void calcCrc() {
        utf16_t* crc = (utf16_t*)this - 1; // get the location of CRC
        size_t size = dynamic offsetof(crc); // calculate packet size
        *crc = crc16_ansi((char*)crc - size, size); // update CRC
    }
}

[
    packetTemplate,
    fixedSize
]
void layoutMyPacket(jnc.DynamicLayout* layout) {
    dylayout (layout) {
        [
            backColor = ui.StdColor.PastelPurple,
            ungroup
        ]
        dyfield TutoProtoHdr hdr;

        [
            backColor = ui.StdColor.PastelGreen,
            ungroup
        ]
        dyfield {
            if (hdr.m_flags & TutoProtoFlags.Error)
                [ ungroup ]
                dyfield TutoProtoError error;
            else
                switch (hdr.m_command) {
                case TutoProtoCommand.GetVersion:
                    if (hdr.m_flags & TutoProtoFlags.Reply)
                        [ ungroup ]
                        dyfield TutoProtoVersion version;
                    break;

                case TutoProtoCommand.Read:
                    if (hdr.m_flags & TutoProtoFlags.Reply)
                        [
                            displayName = "Payload",
                            backColor = ui.StdColor.PastelYellow,
                            binary
                        ]
                        dyfield char data[hdr.m_size - sizeof(TutoProtoHdr)];
                    else
                        [ ungroup ]
                        dyfield TutoProtoRange read;
                    break;

                case TutoProtoCommand.Write:
                    if (hdr.m_flags & TutoProtoFlags.Reply)
                        break;

                    [ ungroup ]
                    dyfield TutoProtoRange write;

                    [
                        displayName = "Payload",
                        backColor = ui.StdColor.PastelYellow,
                        binary
                    ]
                    dyfield char data[write.m_length];
                    break;
                }
        }

        [
            backColor = ui.StdColor.PastelBlue,
            ungroup
        ]
        dyfield {
            [
                displayName = "CRC",
                formatSpec = "0x%04X"
            ]
            dyfield bigendian uint16_t crc;

            [ ungroup ]
            dyfield UpdateCrc updateCrc;
        }
    }
}