Protocol · T-CRH v1.0.1
A routing header that carries deadlines
T-CRH, the Temporal Compact Routing Header, is an IPv6 Routing Header with Routing Type 253, an experimental value from RFC 4727. It extends the Compact Routing Header of RFC 9631 (CRH-16) with a time stamp and one deadline byte per hop.
The normative definition is spec/tcrh-v1.md (version 1.0.1) in the
project repository, with machine-checkable golden test vectors in
spec/golden/. The interactive tools below run a TypeScript port of the reference implementation
in your browser; the reference header, the tables and the golden-vector count were produced by the same
engine when this page was built. If this page and the specification ever disagree, the specification is
right.
In plain words
A list of stops, each with a deadline
The ground station that sends a packet into the constellation, the ingress, writes three things into its T-CRH: the path, as a list of 16-bit segment identifiers (SIDs), one per satellite or ground station; a time budget for every hop, one byte each, counted in small time units; and a time stamp, Epoch_Base, taken when the packet enters the network.
Adding the budgets to the time stamp gives every stop a deadline: the latest time the packet should arrive there if the network behaves as planned. Each satellite named in the header compares its own clock with its own deadline when the packet arrives. No extra messages are needed; the check is a few additions and one comparison.
A late packet means the plan no longer matches reality, for example because a link is up but slow. The satellite sets the DEV flag, writes its hop number into Dev Hop, and applies its deviation policy: by default it stops following the list and forwards the packet geographically towards the final endpoint.
As in RFC 9631, the list is stored backwards: the last stop comes first. Segments Left counts the stops still to visit and is also the index of the next one.
- SID
- Segment identifier: a 16-bit name for a satellite (0x0000 to 0xEFFF) or a ground station (0xF000 + g).
- Deadline unit u
- 1, 0.1, 0.25 or 0.125 ms, chosen by two bits in the Flags byte. Every time value in the header counts units.
- Epoch_Base
- The ingress GPS time counted in deadline units u, rounded up, modulo 2 to the power 32.
- Delta d_k
- The budget for reaching hop k from hop k − 1: one unsigned byte, 0 to 255 units.
- Deadline D_k
- Epoch_Base + d_1 + … + d_k, modulo 2 to the power 32.
- DEV, Dev Hop, GREEDY
- The alarm flag, the hop that first raised it, and the flag that marks geographic fallback.
Anatomy
One header, byte by byte
The reference header from section 10 of the specification: path 2 → 3 → 7, deltas 6, 7 and 8 ms, Epoch_Base 1000, unit 1 ms. Hover over a byte, or Tab into the map and use the arrow keys, to see the field it belongs to and what it means.
T-CRH, 24 bytes: 12 fixed bytes, 3 entries of 3 bytes, 3 bytes of padding.
- fixed field
- mutable in flight
- time (Epoch_Base, deltas)
- SID
- padding
Text version of this header
v0-3hop: 24-byte T-CRH, path 2 → 3 → 7,
unit 1 ms
Label: REAL
spec/golden/tcrh_vectors.json
-
Bytes
0–3
11 02 fd 03Next Header 17 (UDP) · Hdr Ext Len 2 · Routing Type 253 · Segments Left 3 -
Bytes
4–7
00 03 00 00Flags 0x00 (no state flags; unit code 0 = 1 ms) · Num Hops 3 · Dev Hop 0 · Reserved -
Bytes
8–11
00 00 03 e8Epoch_Base 1000 units -
Bytes
12–14
00 07 08SID[0] = 7 · Delta[0] = 8 (final endpoint) -
Bytes
15–17
00 03 07SID[1] = 3 · Delta[1] = 7 -
Bytes
18–20
00 02 06SID[2] = 2 · Delta[2] = 6 (first hop) -
Bytes
21–23
00 00 00Padding to a multiple of 8 bytes (3 B)
| Hop k | SID | Budget d_k | Deadline D_k |
|---|---|---|---|
| 1 | 2 | 6 | 1006 |
| 2 | 3 | 7 | 1013 |
| 3 | 7 | 8 | 1021 |
| Bytes | Field | Meaning |
|---|---|---|
| 0 | Next Header | Protocol of the following header (6 TCP, 17 UDP, 41 IPv6, 59 none). |
| 1 | Hdr Ext Len | Length in 8-byte units after the first 8: size = (Hdr Ext Len + 1) × 8. |
| 2 | Routing Type | 253 (RFC 4727 experimental value). |
| 3 | Segments Left | Entries not yet visited. Mutable. |
| 4 | Flags | DEV, GREEDY, SAT and the unit code. Mutable. |
| 5 | Num Hops | Entries present, 1 to 255; 0 is malformed. |
| 6 | Dev Hop | Hop that first set DEV (0 = the ingress, whose first link was down). Mutable. |
| 7 | Reserved | Zero on transmit, ignored on receipt. |
| 8–11 | Epoch_Base | Ingress time stamp in deadline units. |
| 12 + 3i | SID[i], Delta[i] | 2-byte SID and 1-byte budget, in reverse path order. |
| end | Padding | Zeros up to a multiple of 8 bytes. |
| Mask | Name | Meaning |
|---|---|---|
| 0x01 | DEV | A node found the packet late, or its next link down. Set once, never cleared. |
| 0x02 | GREEDY | The packet is in geographic fallback, not following its SID list. |
| 0x04 | SAT | A delta was clamped to 255 when the header was built. |
| 0x30 | UNIT | Deadline unit code: 0 = 1 ms, 1 = 0.1 ms, 2 = 0.25 ms, 3 = 0.125 ms. |
| 0xC8 | reserved | Zero on transmit, ignored and passed on. |
Three fields change in flight: Segments Left, Flags and Dev Hop. RFC 9631 lets only Segments Left change, which matters for IPsec AH because AH protects the fields that do not change.
Time base
How a deadline is written and checked
Clocks use GPS time, which has no leap seconds. For every date in this project, GPS = UTC + 18 s.
IngressEpoch_Base = ceil(T_ingress / u) mod 2 to the power 32
DeadlineD_k = (Epoch_Base + d_1 + … + d_k) mod 2 to the power 32
Clocknow_u = floor(t_GPS / u) mod 2 to the power 32
Lateserial_diff(now_u, D_k) > 0
Rounding the time stamp up can only add slack. A packet that arrives anywhere inside the deadline unit is on time; it is late only once the clock has moved past that unit. The comparison uses 32-bit serial-number arithmetic, so a deadline just after the counter wraps still compares correctly.
| Code | Unit u | Wrap period | Largest delta (255 u) |
|---|---|---|---|
| 0 | 1 ms | 49.71 days | 255 ms |
| 1 (provisional default) | 0.1 ms | 4.97 days | 25.5 ms |
| 2 | 0.25 ms | 12.43 days | 63.75 ms |
| 3 | 0.125 ms | 6.21 days | 31.875 ms |
Why the budgets are rounded cumulatively
The encoder rounds the cumulative budget up to whole units and stores the difference (spec 5.1).
Rounding errors then never add up, and no deadline is earlier than its budget unless the SAT flag says a
delta was clamped. Version 0 rounded each hop on its own, half to even, which could make a deadline
early. Golden vector half-unit-budgets-uc0 shows the difference at a 1 ms unit:
| Hop k | Cumulative budget | v0 deadline | v1 deadline |
|---|---|---|---|
| 1 | 2.5 ms | 2 ms (early) | 3 ms |
| 2 | 6 ms | 6 ms | 6 ms |
| 3 | 8.4 ms | 8 ms (early) | 9 ms |
| 4 | 11 ms | 11 ms | 11 ms |
Try it
Build a header
Enter a path and a predicted delay for each hop. The cumulative budget of hop k is B_k = S × (delay_1 + … + delay_k); the slack S leaves room for normal variation. The engine stamps Epoch_Base from the ingress time and encodes the budgets with exact arithmetic, never with floating-point rounding.
Examples
Each example is the input of a golden test vector. The SIDs and delays are test values, not a real Starlink route.
Path (10 hops, first hop first)
SIDs are 0 to 65534 in decimal or hex (0xF000 + g is ground station g). The last hop is the final endpoint.
h10-real-gps-time-uc1.The header the engine built. Hover or focus a byte.
- fixed field
- mutable in flight
- time (Epoch_Base, deltas)
- SID
- padding
Hex
11 05 fd 0a 10 0a 00 00 d3 60 8a 73 f0 03 4f 06 55 28 06 54 3a 05 c9 26 05 c8 2c 05 3e 44 05 3d 17 05 3c 36 04 b2 2c 04 b1 23 00 00 00 00 00 00
- Epoch_Base
- 3546319475
- Ingress (GPS ns)
- 1475675898123456000
- Flags
- 0x10 (no state flags)
- Hdr Ext Len
- 5
| k | SID | B_k (ms) | ceil(B_k/u) | d_k | D_k |
|---|---|---|---|---|---|
| 1 | 1201 | 3.445 | 35 | 35 | 3546319510 |
| 2 | 1202 | 7.852 | 79 | 44 | 3546319554 |
| 3 | 1340 | 13.208 | 133 | 54 | 3546319608 |
| 4 | 1341 | 15.548 | 156 | 23 | 3546319631 |
| 5 | 1342 | 22.3145 | 224 | 68 | 3546319699 |
| 6 | 1480 | 26.7215 | 268 | 44 | 3546319743 |
| 7 | 1481 | 30.5968 | 306 | 38 | 3546319781 |
| 8 | 1620 | 36.3259 | 364 | 58 | 3546319839 |
| 9 | 1621 | 40.3715 | 404 | 40 | 3546319879 |
| 10 | 61443 | 48.2144 | 483 | 79 | 3546319958 |
D_k = Epoch_Base + d_1 + … + d_k, modulo 2 to the power 32, in units of 0.1 ms. Ingress time 2026-10-10T13:58:00.123456000Z (UTC); GPS time = UTC + 18 s.
Try it
Decode a header
Paste the bytes of a T-CRH in hex. The engine checks them in the order of spec section 6 step 1 and either names every field or says, in plain words, why a node would reject them.
Examples from the golden test vectors
Validation, in the order of spec section 6 step 1
- PASSAt least 8 bytes
- PASSRouting Type is 253
- PASSNum Hops is not 0
- PASSSegments Left ≤ Num Hops
- PASSHdr Ext Len covers all entries
- PASSEvery announced byte is present
Hover or focus a byte to see its field.
- fixed field
- mutable in flight
- time (Epoch_Base, deltas)
- SID
- padding
| Field | Bytes | Value | Meaning |
|---|---|---|---|
| Next Header | 0 | 17 (UDP) | The protocol of the header that follows the T-CRH. |
| Hdr Ext Len | 1 | 2 | Header length in 8-byte units after the first 8: (2 + 1) × 8 = 24 bytes. |
| Routing Type | 2 | 253 | Marks the routing header as a T-CRH (253 is an RFC 4727 experimental value). |
| Segments Left | 3 | 3 | Entries not yet visited. Equal to Num Hops: a fresh header at the ingress, before its first step. Mutable: each node decrements it. |
| Flags | 4 | 0x00 | no state flags set; deadline unit code 0 = 1 ms. Mutable: DEV and GREEDY are set in flight. |
| Num Hops | 5 | 3 | The path has 3 entries; a parser stops reading there, before the padding. |
| Dev Hop | 6 | 0 | No node has found the packet late. Mutable. |
| Reserved | 7 | 0 | Zero on transmit, ignored on receipt. |
| Epoch_Base | 8–11 | 1000 units | Ingress time stamp in units of 1 ms: ceil(T_ingress / u) mod 2^32. Every deadline counts from here. |
| Padding | 21–23 | 3 B | Zero padding to a multiple of 8 bytes. |
| i | Hop k | SID[i] | Kind | Delta[i] = d_k | D_k |
|---|---|---|---|---|---|
| 0 | 3 | 7 | satellite | 8 | 1021 |
| 1 | 2 | 3 | satellite | 7 | 1013 |
| 2 | 1 | 2 | satellite | 6 | 1006 |
At each satellite
Follow a packet
Only the node named in the IPv6 destination address examines the routing header (RFC 8200). These are its steps, from section 6 of the specification. Step through a packet below to watch each rule fire.
-
Validate
In order: at least 8 bytes; Routing Type 253 (if not, the header is ignored when Segments Left is 0, as RFC 8200 requires, and the packet is discarded otherwise); Num Hops not 0; Segments Left ≤ Num Hops; Hdr Ext Len long enough; all bytes present. A failure discards the packet, with an ICMPv6 Parameter Problem where there is a field to point at.
-
Check the deadline
Unless this is the ingress or DEV is already set: k = Num Hops − Segments Left, and if the clock is past D_k the node sets DEV and writes k into Dev Hop.
-
Deliver at the end
If Segments Left is 0, this node is the final endpoint and delivers the packet, late or not.
-
Apply the deviation policy
If DEV is set: greedy sets GREEDY, sets Segments Left to 0 and forwards geographically towards SID[0]; continue keeps following the list; drop discards the packet.
-
Take the next SID
Decrement Segments Left first, then read SID[Segments Left].
-
Look it up and forward
Look the SID up in the CRH-FIB, the table from SIDs to addresses. No entry: the packet is discarded with an ICMPv6 Parameter Problem, Code 0 (RFC 9631 section 7). Otherwise the destination address becomes the entry’s address and the packet leaves on the entry’s interface.
-
Next link down
If the link to that SID is down, no deadline can show it, so it is a deviation of its own: set DEV (Dev Hop = this hop, 0 at the ingress) and GREEDY, and fall back towards SID[0] under every policy.
Arrival times: actual delay of each hop, in units of 1 ms (edit to try your own)
Step 1 of 5: Ingress ground station. sent. The ingress stamps Epoch_Base = 1000 and writes the path with Segments Left = Num Hops = 3.
Ingress ground station
sent- The ingress stamps Epoch_Base = 1000 and writes the path with Segments Left = Num Hops = 3.
- It decrements Segments Left to 2 and looks up SID[2] = 2, the first hop. The ingress does not check a deadline.
- Segments Left
- 3 → 2
- Hop index k
- 0
- Arrival clock now_u
- not checked at the ingress
- Action
- send
- Next SID
- 2
- Flags after
- 0x00
- Dev Hop after
- 0
Bytes as they leave this step. Changed: byte 3.
- fixed field
- mutable in flight
- time (Epoch_Base, deltas)
- SID
- padding
- changed by this step
| Step | now_u | D_k | Result | Action | SL after | Flags after | Dev Hop |
|---|---|---|---|---|---|---|---|
| Ingress ground station | — | — | sent | send | 2 | none | 0 |
| Hop 1 · SID 2 | 1005 | 1006 | on time | forward | 1 | none | 0 |
| Hop 2 · SID 3 | 1017 | 1013 | late | fallback | 0 | DEV GREEDY | 2 |
| Geographic forwarding | — | — | fallback | transit | 0 | DEV GREEDY | 2 |
| SID 7 · final endpoint | — | 1021 | delivered | deliver | 0 | DEV GREEDY | 2 |
Overhead
What the deadlines cost in bytes
Compared with CRH-16, T-CRH spends one more byte per hop and 8 more fixed bytes. The sizes below come from the formulas in section 9 of the specification. In encapsulation mode the gateway also adds a 40-byte outer IPv6 header (section 8), as SRv6 H.Encaps does.
The slider covers 1 to 40 hops; type a larger n if you need it.
| Header | Formula | n = 1 | n = 3 | n = 5 | n = 10 | n = 13 | n = 20 |
|---|---|---|---|---|---|---|---|
| T-CRH (this project, spec section 2) | pad8(12 + 3n) | 16 | 24 | 32 | 48 | 56 | 72 |
| CRH-16 (RFC 9631) | pad8(4 + 2n) | 8 | 16 | 16 | 24 | 32 | 48 |
| CRH-32 (RFC 9631) | pad8(4 + 4n) | 8 | 16 | 24 | 48 | 56 | 88 |
| SRv6 SRH (RFC 8754, no TLVs) | 8 + 16n | 24 | 56 | 88 | 168 | 216 | 328 |
Compressed SRv6 segment lists (RFC 9800) pack several segment identifiers into one 128-bit entry and can come close to these compact headers in size. They are not in this comparison because their size depends on the compression flavour and the locator layout chosen.
Golden vectors
Checked against the shared test vectors
The Python reference, the ns-3 module and this page's TypeScript engine must all reproduce the same
vectors, byte for byte. The count below was produced while this page was built, by running every vector
in spec/golden/tcrh_vectors.json and crh_vectors.json through the engine.
70 of 70 vectors pass
| Group | Pass | Total |
|---|---|---|
| T-CRH encoding | 16 | 16 |
| T-CRH processing at a node | 32 | 32 |
| T-CRH next link down | 5 | 5 |
| CRH-16/32 encoding (RFC 9631) | 7 | 7 |
| CRH-16/32 processing (RFC 9631) | 10 | 10 |
All 70 vectors
-
pass
v0-3hopns-3 reference vector (spec 10), unit 1 ms -
pass
h1-ground-station-encaph=1 (16 B), final endpoint is ground station 5, Next Header 41 (encapsulation) -
pass
budget-exact-boundary-uc1exactly 3.0 units -> 3; 3 units + 1 ns -> 4; Delta 0 is legal -
pass
sat-uc130 ms hop at 0.1 ms does not fit in a byte: clamp to 255, set SAT; hop 2 catches up -
pass
sat-repeated-uc0two consecutive saturations at 1 ms: 300 ms then 700 ms cumulative -
pass
unit-code-0same budgets and ingress time in unit code 0 (1000000 ns) -
pass
unit-code-1same budgets and ingress time in unit code 1 (100000 ns) -
pass
unit-code-2same budgets and ingress time in unit code 2 (250000 ns) -
pass
unit-code-3same budgets and ingress time in unit code 3 (125000 ns) -
pass
half-unit-budgets-uc0per-hop 2.5/3.5/2.4/2.6 ms: v0 banker's rounding gave 2,4,2,3 (hop 1 early) -
pass
wrap-deadlines-uc0Epoch_Base = 2^32 - 3: D_2 and D_3 wrap past 2^32 -
pass
wrap-epoch-uc1T_ingress one ns before unit 2^32: Epoch_Base = ceil(...) mod 2^32 = 0 -
pass
h10-real-gps-time-uc1h=10 (48 B), GPS time of 2026-10-10T13:58:00.123456Z, budgets = 1.3 x prediction -
pass
h13-uc3h=13 (56 B) at 0.125 ms, Next Header 59 (no next header) -
pass
h255-uc0maximum h=255 (784 B); deadlines wrap past 2^32 part way along -
pass
explicit-sat-flag-uc2deltas given directly with the SAT flag passed in (re-encoding a saturated header) -
pass
on-time-hop1arrival at hop 1 before its deadline -
pass
inside-deadline-unit-hop1arrival inside unit D_1 itself is on time -
pass
late-by-one-unit-hop1-greedyone unit late at hop 1: DEV + GREEDY, SL=0, head for SID[0] -
pass
policy-continue-late-hop2late at hop 2, policy continue: DEV recorded, keep the SID list -
pass
policy-drop-late-hop1late at hop 1, policy drop: discard (late, strict) -
pass
policy-drop-on-time-hop2on time at hop 2: the drop policy does not apply -
pass
final-endpoint-on-timeSL=0 at the final endpoint, inside D_3: deliver unchanged -
pass
final-endpoint-latethe final endpoint also checks: DEV, Dev Hop 3, still delivered -
pass
final-endpoint-late-drop-policystep 3 (deliver) comes before the policy: delivered even under drop -
pass
already-dev-continueDEV already set at hop 1: no second check, Dev Hop stays 1 -
pass
already-dev-greedy-on-timeDEV set upstream: a greedy node converts even an on-time packet -
pass
already-dev-dropDEV set upstream and policy drop: discard although on time here -
pass
greedy-packet-at-final-endpointGREEDY packet reaches SID[0]: deliver, no deadline check -
pass
ingress-sl-equals-numhopsSL = NumHops (ingress): no deadline check, forward to SID[h-1] -
pass
wrap-late-hop1D_1 = 2^32-1, clock wrapped to 0: one unit late -
pass
wrap-on-time-hop2D_2 = 0 (wrapped), clock still at 2^32-1: on time -
pass
wrap-serial-half-range-not-latenow - D_1 = -2^31 exactly: the half-range boundary counts as not late -
pass
wrap-serial-half-range-latenow - D_1 = 2^31 - 1 (mod 2^32): late -
pass
uc3-late-by-one-hop2-continueunit code 3: late by one 0.125 ms unit at hop 2 -
pass
uc3-on-time-finalunit code 3: final endpoint exactly at D_3 -
pass
sat-header-late-hop1SAT header: flags pass through, hop 1 judged on the clamped deadline -
pass
reserved-bits-ignoredreserved flag bits 0x88, Reserved byte and padding nonzero: ignored, kept -
pass
longer-hdr-ext-len-acceptedHEL 3 (32 B) for 3 hops (24 B needed): accepted -
pass
malformed-sl-gt-numhopsSegments Left 4 > Num Hops 3 -
pass
malformed-hdr-ext-len-too-smallHEL 1 (16 B) cannot hold 3 hops (24 B) -
pass
malformed-truncatedHEL says 24 B, only 16 present -
pass
malformed-truncated-fixed-partfewer than 8 bytes -
pass
malformed-wrong-typerouting type 5 (a CRH-16, SL 3) handed to T-CRH processing: RFC 8200 4.4 Parameter Problem, pointer 2 -
pass
not-tcrh-sl0-ignoredrouting type 5 with SL 0: RFC 8200 4.4 says ignore the header and process Next Header (no ICMP) -
pass
malformed-numhops-zeroNum Hops 0: discard, Parameter Problem pointer 5 -
pass
malformed-numhops-zero-sl1Num Hops 0 and SL 1: numhops_zero is checked before SL > NumHops -
pass
dev-hop-0-at-final-endpointDEV|GREEDY with Dev Hop 0 (set by the ingress, link down): valid, delivered -
pass
ingress-link-downthe ingress finds the link to s_1 down: DEV with Dev Hop 0 (spec 2) -
pass
hop1-link-downnode 1 forwarded on time, then finds its next link down: Dev Hop 1 -
pass
dev-already-set-continue-link-downDEV set at hop 2 under 'continue', next link down: Dev Hop stays 2, GREEDY added -
pass
sat-header-ingress-link-downSAT and UNIT bits pass through (0x14 -> 0x17) -
pass
link-down-malformed-numhops-zerobytes that fail step 1 are discarded as in process -
pass
ns3-crh16-3hopns-3 crh-experiment.cc self-test vector -
pass
crh16-2hoptwo SIDs fill the first 8 octets exactly -
pass
crh16-5hop5 SIDs -> 16 B -
pass
crh16-10hop10 SIDs -> 24 B -
pass
crh16-13hop13 SIDs -> 32 B -
pass
crh32-1hopCRH-32, one 32-bit SID -> 8 B -
pass
crh32-3hopCRH-32 version of the ns-3 path -
pass
walk-sl3-sourcesource: decrement to 2, then index -> SID[2] = 2 -
pass
walk-sl2SL 2 -> 1, SID[1] = 3 -
pass
walk-sl1SL 1 -> 0, SID[0] = 7 -
pass
walk-sl0-deliverSL 0: this node is the destination -
pass
ns3-forged-sl62-SID header (HEL 0) with SL forged to 6: L = 1 > 0, ICMP code 6 -
pass
crh32-forged-sl21-SID CRH-32 (HEL 0) with SL 2: L = 1 > 0 -
pass
crh16-sl6-fits6 SIDs fit in 16 B (HEL 1): L = ceil(4/4) = 1, accepted -
pass
wrong-typea T-CRH (type 253, SL 3) handed to CRH processing -
pass
wrong-type-sl0-ignoreda T-CRH with SL 0 handed to CRH processing: RFC 8200 4.4, ignored -
pass
truncatedHEL says 16 B, only 8 present
See also the methodology and the specification in the repository.