Writing a Packet Template (static structs)
In this tutorial, we’ll show how to define your own packet templates that can later be conveniently filled via a property grid.
Simple Packet
Inside the Transmit pane, switch to the Binary Tab, and click the Use packet template
button:
The property grid and the packet template selector will appear:
The packet template selector is empty, because there are no packet templates defined yet. Click the Edit packet template
button, and the Packet Template Editor dialog will appear. Here you can describe the templates for your packets by defining C-like struct
-s.
Copy paste the following code:
pragma(Alignment, 1);
[ packetTemplate ]
struct MyPacket {
uint8_t m_byte;
uint16_t m_word;
uint32_t m_dword;
uint64_t m_qword;
}
This will yield the following result:
You can experiment with filling the fields both in the property grid and in the hex-editor and observe how they synchronize automatically.
Note
When filling in a field vie the property grid, you can use both decimal and hexadecimal (prefixed with 0x
) notations.
Pay attentrion to the Alignment
declaration. Most times, you want to start your packet template script with the following line:
pragma(Alignment, 1);
This is to prevent the Jancy compiler from inserting implicit paddings to try and align fields on the default 8-byte boundaries. By default, Jancy aligns fields just like most modern C-compilers do (they use the 8-byte alignment by default). The detailed description of what alignment is for and how it works is beyond the scope of this document. Just remember that when you notice some unexpected offsets for your fields, it most likely means that you’ve forgotten about the Alignment
directive.
Big-endian Fields
More often than not, network protocols are using the big-endian format for integers. It means, the most-significant byte is transmitted first, and the least-signinficant byte is transmitted last. Because this is such a common thing in network protocols, this byte order is even called the network byte order!
At the same time, on Intel archictures that dominate the PC market, integers are natively representned using the little-endian format – that is, when an integer is stored in memory, the least-significant byte occupies the lower memory address, and the most-significant byte goes into the higher address.
What that means, is that byte order conversions are simply ubiquitos in network programming, and because they are used in so many places, it’s super-easy to forget about it in once or twice – and that all it takes for your program to malfunction!
Jancy, being the language that targets the IO programming, has the native support for big-endians. All you have to do is to declare a field as bigendian
, and all the necessary byte-order conversions will happen automatically behind the scene!
Try changing your packet struct
as such:
[ packetTemplate ]
struct MyPacket {
uint8_t m_byte;
bigendian uint16_t m_word;
bigendian uint32_t m_dword;
bigendian uint64_t m_qword;
}
Then observe the effects of this change by modifying values of the last three fields.
Enumerated Fields
Sometimes, a field does not carry an arbitrary number, but rather can only hold a value from some predefined enumeration. For example, the EtherType
field of the Ethernet II
header typically only holds one of the values defined here: https://en.wikipedia.org/wiki/EtherType
When you fill your packet using the property grid, it’s desireable to have such fields represented by combo-boxes with a fixed set of drop-down values.
In order to achieve this, define an enumeration as such:
enum MyCommand: uint16_t {
Handshake,
Play,
Pause,
Stop,
}
Then add a field of this type to your packet template:
[ packetTemplate ]
struct MyPacket {
bigendian MyCommand m_command;
...
}
User Actions
Filling packet fields with the property grid is both very convenient and much less error-prone than writing it by hand in the hex-editor – now, after you’ve tried that for yourself, you will most certainly agree.
However, certain things are still hard to fill even with the property grid – the checksum field would be the first thing that comes to mind here. In many protocols, after you’ve filled all the fields of a packet, you need to calculate the checksum and add it to the packet, as well – otherwise, the receiving party will simply dismiss your packet as an invalid one.
To save the day, here come the custom actions.
Besides fields, your packet template can also define user-invokable methods which can be programmatically modify the packet as necessary.
To mark a method as user-invokable, add the [ userAction ]
attribute as such:
import "crc16.jnc"
[ packetTemplate ]
struct MyPacket {
bigendian MyCommand m_command;
uint8_t m_byte;
bigendian uint16_t m_word;
bigendian uint32_t m_dword;
bigendian uint64_t m_qword;
bigendian uint16_t m_checksum;
[ userAction ]
void calcChecksum() {
m_checksum = crc16_ansi(this, offsetof(MyPacket.m_checksum));
}
}
After that you will see a new clickable hyperlink called calcChecksum
, which will update the m_checksum
field appropriately. The end result can be seen below:
You can also specify a human-readable description for the user action by adding it as an attribute:
[ userAction = "Calculate checksum" ]
void calcChecksum() {
...
}
Autorun Actions
In the above sample with a checksum, you migh want to automatically update the checksum before transmission. Luckily, IO Ninja provides autorun actions just for that. Each action marked with the [ autorun ]
attribute will automatically be executed each time the packet changes.
[
userAction = "Calculate checksum",
autorun = "Auto-update checksum"
]
void calcChecksum() {
...
}
This is a great way of ensuring validity of your outbound packets before transmission.
However, you always remain in control and can transmit a “broken” checksum if you need to. In order to do so, disable auto-execution of a particular action by unchecking the “Auto-update” checkbox.