You would never break the chain (Part 10/12) 🕊️

Paul Tagliamonte 2026-09-08 diy
🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

Now that we have a working Layer 1, we have a way to send a block of bits from one place to anyone who cares to listen to us. This is very welcome news, but we are now facing a new, different and just as fun question – what shape should that data take?

⏳ Need a bit more of a crash course on what "Layer 1" and "Layer 2" mean? No problem, I wrote up short summary here to help.

Given our incredibly limited functionality of our nodes, we could definitely skip all this work and just stuff an IP packet into the link; but I decided to not since I am (eventually) interested in adding some sort of spanning tree-like protocol to implement network switching so not all nodes need to communicate directly with all other nodes – but that day is not today.

Given i’m going to stub most of that out, let’s take a look at what some similar Layer 2 protocols use – things like Ethernet or WiFi frames. Both contain structured information regarding the transmitter, desired recipient, type of data, and the higher-level data itself (such as IP packets). As a result of attempting to learn from others, the Pigeon Layer 2 (called, simply, “link”) is also split into a fixed-length header, followed by the contents described by the header.

dst mac
src mac
callsign
length
payload

The header is a fixed-length (23 byte) structure, which contains the source MAC address (src mac), destination MAC address (dst mac), the ITU coordinated ham radio callsign of the control operator of this message (callsign), the type of payload to follow (type; defined below), and the length of the data to follow the header (length as a 16 bit big-endian unsigned integer).

The type field indicates how the payload is to be interpreted – currently I’ve only defined 3 possible payload types so far:

TypeDescription
0x01Raw (testing only)
0x02Cal
0x04Ipv6

Keen observers will perhaps infer that there used to be an Ipv4 type at 0x03 – which is true – however, i’ve since removed it since i’ve never once used it and the codepath was more trouble than it was worth. As is my wont, I’ve optend to just lean into Ipv6-only IP transport – it’s easy enough to shim ipv4 in, if someone REALLY wanted to using something like 64:ff9b:1::/48 and a bit of code in the transmitter/receiver (or even using something like jool and unbound’s dns64-prefix at the router). I don’t think I’ll bring it back, but just in case I have to for some reason in the future, it’s there.

Additionally, friends of the pod may also recognize that this structure is basically the exact same structure as what I had in PACKRAT, which, is also true. I started this project off maintaining interoperability in the Layer 2 for pigeon and packrat, but at some point just gave up on it during one of the many cleanups. I’m hopeful I can maintain compatibility going forward, and that won’t have to muck with this header too much more. We’ll see what happens once I start to push the bounds of what is possible with pigeon.

Hopefully it feels like carrying IP data inside this frame to be a pretty self-explanatory exercise – the 0th byte of the payload is the 0th byte of an IPv6 header (followed by all the usual stuff, like UDP or TCP header(s) and any carried data, just like you’d find anywhere else.

Pigeon’s calabration protocol →