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.

Label: REAL definitions from spec/tcrh-v1.md
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.

Label: REAL built by the engine, identical to golden vector v0-3hop

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
Golden vector v0-3hop: 24-byte T-CRH, path 2 → 3 → 7, unit 1 ms Label: REAL spec/golden/tcrh_vectors.json
  1. Bytes 0–3 11 02 fd 03 Next Header 17 (UDP) · Hdr Ext Len 2 · Routing Type 253 · Segments Left 3
  2. Bytes 4–7 00 03 00 00 Flags 0x00 (no state flags; unit code 0 = 1 ms) · Num Hops 3 · Dev Hop 0 · Reserved
  3. Bytes 8–11 00 00 03 e8 Epoch_Base 1000 units
  4. Bytes 12–14 00 07 08 SID[0] = 7 · Delta[0] = 8 (final endpoint)
  5. Bytes 15–17 00 03 07 SID[1] = 3 · Delta[1] = 7
  6. Bytes 18–20 00 02 06 SID[2] = 2 · Delta[2] = 6 (first hop)
  7. Bytes 21–23 00 00 00 Padding to a multiple of 8 bytes (3 B)
Cumulative arrival deadline at each hop: D_k = Epoch_Base + d_1 + … + d_k (mod 2 to the power 32), in units of 1 ms.
Hop k SID Budget d_k Deadline D_k
1 2 6 1006
2 3 7 1013
3 7 8 1021
Fields (spec section 2). Multi-byte fields are big-endian.
BytesFieldMeaning
0Next HeaderProtocol of the following header (6 TCP, 17 UDP, 41 IPv6, 59 none).
1Hdr Ext LenLength in 8-byte units after the first 8: size = (Hdr Ext Len + 1) × 8.
2Routing Type253 (RFC 4727 experimental value).
3Segments LeftEntries not yet visited. Mutable.
4FlagsDEV, GREEDY, SAT and the unit code. Mutable.
5Num HopsEntries present, 1 to 255; 0 is malformed.
6Dev HopHop that first set DEV (0 = the ingress, whose first link was down). Mutable.
7ReservedZero on transmit, ignored on receipt.
8–11Epoch_BaseIngress time stamp in deadline units.
12 + 3iSID[i], Delta[i]2-byte SID and 1-byte budget, in reverse path order.
endPaddingZeros up to a multiple of 8 bytes.
Flags byte (spec section 3)
MaskNameMeaning
0x01DEVA node found the packet late, or its next link down. Set once, never cleared.
0x02GREEDYThe packet is in geographic fallback, not following its SID list.
0x04SATA delta was clamped to 255 when the header was built.
0x30UNITDeadline unit code: 0 = 1 ms, 1 = 0.1 ms, 2 = 0.25 ms, 3 = 0.125 ms.
0xC8reservedZero 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.

Label: REAL tables: exact values from the spec, computed at build time

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.

Unit codes, and how long the 32-bit counter runs before it wraps (2 to the power 32 × u), computed from the spec
CodeUnit uWrap periodLargest 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:

Per-hop budgets 2.5, 3.5, 2.4 and 2.6 ms. Deadlines in ms after Epoch_Base; a deadline below its budget is early. Computed at build time: v1 by the engine, v0 by per-hop round-half-even.
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.

Label: REAL bytes: the engine's output Label: MODELLED inputs: example values you choose

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.

VALIDA 48-byte T-CRH with 10 hops. Byte-identical to golden vector 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
How each delta is chosen (spec 5.1): target = ceil(B_k / u), d_k = target − (sum of earlier deltas), clamped to 255. Unit u = 0.1 ms.
kSIDB_k (ms)ceil(B_k/u)d_kD_k
112013.44535353546319510
212027.85279443546319554
3134013.208133543546319608
4134115.548156233546319631
5134222.3145224683546319699
6148026.7215268443546319743
7148130.5968306383546319781
8162036.3259364583546319839
9162140.3715404403546319879
106144348.2144483793546319958

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

VALIDA 24-byte T-CRH with 3 hops. A fresh header: the ingress has not sent it yet.

Validation, in the order of spec section 6 step 1

  1. PASSAt least 8 bytes
  2. PASSRouting Type is 253
  3. PASSNum Hops is not 0
  4. PASSSegments Left ≤ Num Hops
  5. PASSHdr Ext Len covers all entries
  6. 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
Fixed fields and padding
FieldBytesValueMeaning
Next Header017 (UDP)The protocol of the header that follows the T-CRH.
Hdr Ext Len12Header length in 8-byte units after the first 8: (2 + 1) × 8 = 24 bytes.
Routing Type2253Marks the routing header as a T-CRH (253 is an RFC 4727 experimental value).
Segments Left33Entries not yet visited. Equal to Num Hops: a fresh header at the ingress, before its first step. Mutable: each node decrements it.
Flags40x00no state flags set; deadline unit code 0 = 1 ms. Mutable: DEV and GREEDY are set in flight.
Num Hops53The path has 3 entries; a parser stops reading there, before the padding.
Dev Hop60No node has found the packet late. Mutable.
Reserved70Zero on transmit, ignored on receipt.
Epoch_Base8–111000 unitsIngress time stamp in units of 1 ms: ceil(T_ingress / u) mod 2^32. Every deadline counts from here.
Padding21–233 BZero padding to a multiple of 8 bytes.
Entries in wire order (reverse path order). Unit u = 1 ms. Flags: none (0x00).
iHop kSID[i]KindDelta[i] = d_kD_k
037satellite81021
123satellite71013
212satellite61006

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.

Label: REAL decisions: the engine's output Label: MODELLED arrival times, down links: example inputs
  1. 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.

  2. 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.

  3. Deliver at the end

    If Segments Left is 0, this node is the final endpoint and delivers the packet, late or not.

  4. 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.

  5. Take the next SID

    Decrement Segments Left first, then read SID[Segments Left].

  6. 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.

  7. 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.

Scenario (example inputs, not measurements)
Arrival times: actual delay of each hop, in units of 1 ms (edit to try your own)
Step 1 of 5

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
Every step of this packet (the engine's output for the inputs above)
Stepnow_uD_kResultActionSL afterFlags afterDev Hop
Ingress ground station——sentsend2none0
Hop 1 · SID 210051006on timeforward1none0
Hop 2 · SID 310171013latefallback0DEV GREEDY2
Geographic forwarding——fallbacktransit0DEV GREEDY2
SID 7 · final endpoint—1021delivereddeliver0DEV GREEDY2

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.

Label: REAL exact sizes from the RFC and spec formulas
Mode (spec section 8)

The slider covers 1 to 40 hops; type a larger n if you need it.

Bytes added to each packet with 10 hops, source insertion
  • T-CRH this project, spec section 248 B
  • CRH-16 RFC 963124 B
  • CRH-32 RFC 963148 B
  • SRv6 SRH RFC 8754, no TLVs168 B
Routing header size in bytes (source insertion). pad8(x) rounds x up to a multiple of 8.
HeaderFormulan = 1n = 3n = 5n = 10n = 13n = 20
T-CRH (this project, spec section 2)pad8(12 + 3n)162432485672
CRH-16 (RFC 9631)pad8(4 + 2n)81616243248
CRH-32 (RFC 9631)pad8(4 + 4n)81624485688
SRv6 SRH (RFC 8754, no TLVs)8 + 16n245688168216328

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.

Label: REAL our engine's output when this page was built

70 of 70 vectors pass

By group
GroupPassTotal
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-3hop ns-3 reference vector (spec 10), unit 1 ms
  • pass h1-ground-station-encap h=1 (16 B), final endpoint is ground station 5, Next Header 41 (encapsulation)
  • pass budget-exact-boundary-uc1 exactly 3.0 units -> 3; 3 units + 1 ns -> 4; Delta 0 is legal
  • pass sat-uc1 30 ms hop at 0.1 ms does not fit in a byte: clamp to 255, set SAT; hop 2 catches up
  • pass sat-repeated-uc0 two consecutive saturations at 1 ms: 300 ms then 700 ms cumulative
  • pass unit-code-0 same budgets and ingress time in unit code 0 (1000000 ns)
  • pass unit-code-1 same budgets and ingress time in unit code 1 (100000 ns)
  • pass unit-code-2 same budgets and ingress time in unit code 2 (250000 ns)
  • pass unit-code-3 same budgets and ingress time in unit code 3 (125000 ns)
  • pass half-unit-budgets-uc0 per-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-uc0 Epoch_Base = 2^32 - 3: D_2 and D_3 wrap past 2^32
  • pass wrap-epoch-uc1 T_ingress one ns before unit 2^32: Epoch_Base = ceil(...) mod 2^32 = 0
  • pass h10-real-gps-time-uc1 h=10 (48 B), GPS time of 2026-10-10T13:58:00.123456Z, budgets = 1.3 x prediction
  • pass h13-uc3 h=13 (56 B) at 0.125 ms, Next Header 59 (no next header)
  • pass h255-uc0 maximum h=255 (784 B); deadlines wrap past 2^32 part way along
  • pass explicit-sat-flag-uc2 deltas given directly with the SAT flag passed in (re-encoding a saturated header)
  • pass on-time-hop1 arrival at hop 1 before its deadline
  • pass inside-deadline-unit-hop1 arrival inside unit D_1 itself is on time
  • pass late-by-one-unit-hop1-greedy one unit late at hop 1: DEV + GREEDY, SL=0, head for SID[0]
  • pass policy-continue-late-hop2 late at hop 2, policy continue: DEV recorded, keep the SID list
  • pass policy-drop-late-hop1 late at hop 1, policy drop: discard (late, strict)
  • pass policy-drop-on-time-hop2 on time at hop 2: the drop policy does not apply
  • pass final-endpoint-on-time SL=0 at the final endpoint, inside D_3: deliver unchanged
  • pass final-endpoint-late the final endpoint also checks: DEV, Dev Hop 3, still delivered
  • pass final-endpoint-late-drop-policy step 3 (deliver) comes before the policy: delivered even under drop
  • pass already-dev-continue DEV already set at hop 1: no second check, Dev Hop stays 1
  • pass already-dev-greedy-on-time DEV set upstream: a greedy node converts even an on-time packet
  • pass already-dev-drop DEV set upstream and policy drop: discard although on time here
  • pass greedy-packet-at-final-endpoint GREEDY packet reaches SID[0]: deliver, no deadline check
  • pass ingress-sl-equals-numhops SL = NumHops (ingress): no deadline check, forward to SID[h-1]
  • pass wrap-late-hop1 D_1 = 2^32-1, clock wrapped to 0: one unit late
  • pass wrap-on-time-hop2 D_2 = 0 (wrapped), clock still at 2^32-1: on time
  • pass wrap-serial-half-range-not-late now - D_1 = -2^31 exactly: the half-range boundary counts as not late
  • pass wrap-serial-half-range-late now - D_1 = 2^31 - 1 (mod 2^32): late
  • pass uc3-late-by-one-hop2-continue unit code 3: late by one 0.125 ms unit at hop 2
  • pass uc3-on-time-final unit code 3: final endpoint exactly at D_3
  • pass sat-header-late-hop1 SAT header: flags pass through, hop 1 judged on the clamped deadline
  • pass reserved-bits-ignored reserved flag bits 0x88, Reserved byte and padding nonzero: ignored, kept
  • pass longer-hdr-ext-len-accepted HEL 3 (32 B) for 3 hops (24 B needed): accepted
  • pass malformed-sl-gt-numhops Segments Left 4 > Num Hops 3
  • pass malformed-hdr-ext-len-too-small HEL 1 (16 B) cannot hold 3 hops (24 B)
  • pass malformed-truncated HEL says 24 B, only 16 present
  • pass malformed-truncated-fixed-part fewer than 8 bytes
  • pass malformed-wrong-type routing type 5 (a CRH-16, SL 3) handed to T-CRH processing: RFC 8200 4.4 Parameter Problem, pointer 2
  • pass not-tcrh-sl0-ignored routing type 5 with SL 0: RFC 8200 4.4 says ignore the header and process Next Header (no ICMP)
  • pass malformed-numhops-zero Num Hops 0: discard, Parameter Problem pointer 5
  • pass malformed-numhops-zero-sl1 Num Hops 0 and SL 1: numhops_zero is checked before SL > NumHops
  • pass dev-hop-0-at-final-endpoint DEV|GREEDY with Dev Hop 0 (set by the ingress, link down): valid, delivered
  • pass ingress-link-down the ingress finds the link to s_1 down: DEV with Dev Hop 0 (spec 2)
  • pass hop1-link-down node 1 forwarded on time, then finds its next link down: Dev Hop 1
  • pass dev-already-set-continue-link-down DEV set at hop 2 under 'continue', next link down: Dev Hop stays 2, GREEDY added
  • pass sat-header-ingress-link-down SAT and UNIT bits pass through (0x14 -> 0x17)
  • pass link-down-malformed-numhops-zero bytes that fail step 1 are discarded as in process
  • pass ns3-crh16-3hop ns-3 crh-experiment.cc self-test vector
  • pass crh16-2hop two SIDs fill the first 8 octets exactly
  • pass crh16-5hop 5 SIDs -> 16 B
  • pass crh16-10hop 10 SIDs -> 24 B
  • pass crh16-13hop 13 SIDs -> 32 B
  • pass crh32-1hop CRH-32, one 32-bit SID -> 8 B
  • pass crh32-3hop CRH-32 version of the ns-3 path
  • pass walk-sl3-source source: decrement to 2, then index -> SID[2] = 2
  • pass walk-sl2 SL 2 -> 1, SID[1] = 3
  • pass walk-sl1 SL 1 -> 0, SID[0] = 7
  • pass walk-sl0-deliver SL 0: this node is the destination
  • pass ns3-forged-sl6 2-SID header (HEL 0) with SL forged to 6: L = 1 > 0, ICMP code 6
  • pass crh32-forged-sl2 1-SID CRH-32 (HEL 0) with SL 2: L = 1 > 0
  • pass crh16-sl6-fits 6 SIDs fit in 16 B (HEL 1): L = ceil(4/4) = 1, accepted
  • pass wrong-type a T-CRH (type 253, SL 3) handed to CRH processing
  • pass wrong-type-sl0-ignored a T-CRH with SL 0 handed to CRH processing: RFC 8200 4.4, ignored
  • pass truncated HEL says 16 B, only 8 present

See also the methodology and the specification in the repository.