diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index aa77e411a6..585ba6b107 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -17,7 +17,8 @@
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
"https://bcr.bazel.build/modules/apple_support/1.13.0/MODULE.bazel": "7c8cdea7e031b7f9f67f0b497adf6d2c6a2675e9304ca93a9af6ed84eef5a524",
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
- "https://bcr.bazel.build/modules/apple_support/1.15.1/source.json": "517f2b77430084c541bc9be2db63fdcbb7102938c5f64c17ee60ffda2e5cf07b",
+ "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442",
+ "https://bcr.bazel.build/modules/apple_support/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2",
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f",
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f",
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
@@ -43,8 +44,10 @@
"https://bcr.bazel.build/modules/bazel_features/1.20.0/MODULE.bazel": "8b85300b9c8594752e0721a37210e34879d23adc219ed9dc8f4104a4a1750920",
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
"https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a",
- "https://bcr.bazel.build/modules/bazel_features/1.23.0/source.json": "c72c61b722d7c3f884994fe647afeb2ed1ae66c437f8f370753551f7b4d8be7f",
+ "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
+ "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
+ "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
@@ -155,7 +158,8 @@
"https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/8.11.0/MODULE.bazel": "c3d280bc5ff1038dcb3bacb95d3f6b83da8dd27bba57820ec89ea4085da767ad",
- "https://bcr.bazel.build/modules/rules_java/8.11.0/source.json": "302b52a39259a85aa06ca3addb9787864ca3e03b432a5f964ea68244397e7544",
+ "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615",
+ "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
@@ -232,7 +236,6 @@
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
- "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
diff --git a/doc/dev/design/Hummingbird.md b/doc/dev/design/Hummingbird.md
new file mode 100644
index 0000000000..990e56e58f
--- /dev/null
+++ b/doc/dev/design/Hummingbird.md
@@ -0,0 +1,424 @@
+# Hummingbird QoS Protocol
+
+
+- Author: Juan A. Garcia-Pardo
+- Last updated: 2025-11-20
+- Discussion at: -
+
+## Abstract
+
+Hummingbird is a QoS protocol that runs on top of SCION.
+It allows ASes to list part of their bandwidth in marketplaces,
+and clients to purchase this bandwidth via reservations.
+The protocol consists of three distinct planes:
+
+- Control-plane.
+- Marketplace.
+- Data-plane.
+
+The control-plane is inherited from the SCION control-plane,
+and allows ASes and clients to find paths between source and destination pairs.
+Additionally, ASes can create *flyovers* between an ingress and an egress interface,
+with certain bandwidth and validity time (start-time and end-time of the flyover).
+These flyovers are then listed in a marketplace in the form of *assets*,
+and made available for clients to purchase.
+
+At the marketplace, when a client purchases an asset,
+it has the right to sell it again, or to convert it into a reservation.
+A reservation is created from an asset via a process of *redemption*,
+where the client contacts the owning AS with the asset and the client information,
+morphing the asset into an intransferrable reservation,
+valid only for that particular client.
+Reservations are not tradeable anymore, as they work only for the client that redeemed the asset.
+
+Once the reservation is created,
+it can be used on a path that transits the owning AS using the ingress-egress pair of the reservation.
+If the reservation is used following the specified parameters of bandwidth and validity time,
+the packets transiting that AS are guaranteed to be forwarded without losses.
+
+
+
+
+
+
+ This path between a source end-host in AS A, and a destination end-host in AS D,
+ contains only one flyover, guaranteed transit at AS B through interfaces 1馃2,
+ with bandwidth 3Mbps, and valid between 13:20 and 13:40 (full timestamp not shown for brevity).
+ Note that the transit through AS C does not have a reservation, thus packets may be dropped there.
+
+
+
+The interactions with the marketplace is out of scope of both this document
+and the implementation of Hummingbird for SCION in `scionproto`.
+This interaction includes the following:
+
+- Publishing assets into the marketplace.
+- Purchasing assets from the marketplace.
+- Redemption of assets into reservations.
+
+
+## Control-plane
+Once the reservations are obtained by the client,
+they can be assembled into existing paths to create a fully or partially reserved path:
+fully when all hops contain reservations, partially reserved when not.
+
+Obtaining paths from source to destination is done by means of the SCION control-plane,
+and it is typically done by the clients before obtaining the reservations.
+Once a regular SCION path exists, and the reservations are obtained,
+there is a process of merging both together.
+This yields a reserved path, that is usable by the data-plane.
+
+
+## Data-plane
+The data-plane in Hummingbird is organized very similarly to the regular SCION case.
+Border routers do not keep state for forwarding other than the token buckets to control the bandwidth usage,
+and one secret key.
+This key is rotated similarly like the master secret is rotated in the regular SCION case.
+
+The rest of this section is organized as follows:
+
+- Description of the wire format of Hummingbird packets.
+- Processing steps of the border router when forwarding a Hummingbird packet.
+
+### Wire Format
+A packet using the Hummingbird protocol is similar in structure to a regular SCION one,
+and contains the following:
+
+- A path metaheader.
+- Between 1 and 3 info fields.
+- Between 1 and 52* flyover or hop fields.
+
+*: There is a maximum of 52 flyovers possible on any given path, which comes from the
+encoding of the CurrHF. See _CurrHF_ in the Metaheader below for details.
+
+
+
+
+#### Metaheader
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| C | CurrHF |r| Seg0Len | Seg1Len | Seg2Len |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| BaseTimeStamp |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| MillisTimestamp | Counter |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+
+- **(C)urrINF**: 2-bit index (0-based) pointing to the current info field (see offset calculations below).
+- **CurrHF (changed)**: 8-bit index (0-based) pointing to the start of the current hop field
+ (see offset calculations below) in 4-byte increments.
+ This index is increased by 3 for normal hop fields and by 5 for flyover hop fields,
+ which are 12 B and 20 B long, respectively.
+ This field can represent $1 + (2^8 / 5) = 52$ flyovers,
+ or $1 + (2^8 / 3) = 86$ hop fields (without reservations).
+- **r**: Unused and reserved for future use.
+- **Seg{0,1,2}Len (changed)**: 7-bit encoding of the length of each segment.
+ The value in these fields is the length of the respective segment in bytes divided by 4.
+ Seg[i]Len > 0 implies the existence of info field i.
+ If a given Seg[i]Len is zero, all subsequent Seg[j]Len with j > i will also be zero.
+ Each Seg[i]Len field can represent $2^7/5 = 25$ flyovers,
+ or $2^7 / 3 = 42$ hop fields (without reservations).
+- **BaseTimestamp (new)**: A unix timestamp (unsigned integer, 1-second granularity, similar to beacon timestamp in normal SCION path segments) that is used as a base to calculate start times for flyovers and the high granularity MillisTimestamp.
+- **MillisTimestamp (new)**: Millisecond granularity timestamp, as offset from BaseTimestamp. Used to compute MACs for flyover hops and to check recentness of a packet.
+- **Counter (new)**: A counter for each packet that is sent by the source to ensure that the tuple (BaseTimestamp, MillisTimestamp, Counter) is unique. This can then be used for the optional duplicate suppression at an AS.
+
+
+The number of info fields present in the path is calculated as the amount of
+Seg[i]Len that are greater than zero, based on the following:
+
+```go
+function InfFieldCount(){
+ if Seg2Len != 0:
+ return 3
+ else if Seg1Len != 0:
+ return 2
+ else if Seg0Len != 0:
+ return 1
+ else
+ // All fields are zero. Intra-AS path.
+ return 0
+}
+NumInf = InfFieldCount()
+```
+
+The path offsets are computed as:
+
+- InfoFieldOffset = 12 B + 8 B 路 CurrINF
+- HopFieldOffset = 12 B + 8 B 路 NumINF + 4 B 路 CurrHF
+
+
+#### InfoField
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|r r r r r r P C| RSV | SegID |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| TimeStamp |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+- **r**: Unused and reserved for future use.
+- **P**: Peering flag. If set to true, then the forwarding path is built as a peering path, which requires special processing on the data plane.
+- **C**: Construction direction flag. If set to true then the hop fields are arranged in the direction they have been constructed during beaconing.
+- **RSV**: Unused and reserved for future use.
+- **SegID**: Updatable field used in the MAC-chaining mechanism.
+- **Timestamp**: Timestamp created by the initiator of the corresponding beacon. The timestamp is expressed in Unix time, and is encoded as an unsigned integer within 4 bytes with 1-second time granularity. It enables validation of the hop field by verification of the expiration time and MAC.
+
+
+#### Regular HopField
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|F r r r r r I E| ExpTime | ConsIngress |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ConsEgress | |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+| HopFieldMAC |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+- **F (new)**: Flyover bit. Indicates whether this is a hop field or a flyover hop field. Set to 0 for HopFields.
+- **r (unchanged)**: Unused and reserved for future use.
+- **I (unchanged)**: ConsIngress Router Alert. If the ConsIngress Router Alert is set, the ingress router (in construction direction) will process the L4 payload in the packet.
+- **E (unchanged)**: ConsEgress Router Alert. If the ConsEgress Router Alert is set, the egress router (in construction direction) will process the L4 payload in the packet. ExpTime (unchanged) Expiry time of a hop field. The field is 1-byte long, thus there are 256 different values available to express an expiration time. The expiration time expressed by the value of this field is relative, and an absolute expiration time in seconds is computed in combination with the timestamp field (from the corresponding info field).
+- **ConsIngress, ConsEgress (unchanged)**: The 16-bit interface IDs in construction direction.
+- **HopFieldMAC (name changed)**: 6-byte MAC to authenticate the hop field. For details on how this MAC is calculated refer to the hop-field MAC computation of the SCION path type.
+
+
+#### FlyoverHopField
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|F r r r r r I E| ExpTime | ConsIngress |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ConsEgress | |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+| AggMAC |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ResID | BW |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ResStartOffset | ResDuration |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+- **F Flyover bit**: Indicates whether this is a hop field or a flyover hop field. Set to 1 for FlyoverHopFields.
+- **r, I, E, ExpTime, ConsIngress, ConsEgress**: These values are the same as in the standard HopField. Note that ExpTime is the expiration time of the standard HopField, not the expiration time of the reservation.
+- **AggMAC**: See below.
+- **ResID**: 22-bit Reservation ID, this allows for approximately 4 million concurrent reservations for a given ingress/egress pair.
+- **BW**: 10-bit bandwidth field indicating the reserved bandwidth which allows for 1024 different values. The values could be encoded similarly to floating point numbers (but without negative numbers or fractions), where some bits encode the exponent and some the significant digits. For example, one can use 5 bits for the exponent and 5 bits for the significand and calculate the value as significand if exponent = 0 or (32 + significand) << (exponent - 1) otherwise. This allows values from 0 to almost $2^{36}$ with an even spacing for each interval between powers of 2.
+- **ResStartOffset**: The offset between the BaseTimestamp in the Path Meta header and the start of the reservation (in seconds). This allows values up to approximately 18 hours in second granularity.
+- **ResDuration**: Duration of the reservation, i.e., the difference between the timestamps of the start and expiration time of the reservation.
+
+
+### Forwarding Steps for a Hummingbird packet
+When a packet arrives at the ingress queue of a border router,
+the first step after dequeuing it is to determine its path type.
+If the path type is Hummingbird, the border router can determine the following actions:
+
+- Forward it with priority (reserved transit).
+- Forward it as best effort.
+- Drop the packet.
+
+The following steps are used to determine each action.
+During processing, if an inconsistency is found in the packet (declared sizes overflowing the packet, etc.),
+the packet is dropped. (left out of the sequence for clarity)
+
+1. If this hop is marked as a flyover:
+ 1. Compute the FlyoverMAC. The FlyoverMAC is used to further compute the aggregated MAC (AggMAC).
+ 1. If the reserved flyover is no longer valid, the packet will be best-effort.
+ 1. The packet is marked internally as guaranteed forwarding.
+1. Compute the regular SCION hop field MAC (HopFieldMAC) and compare with that of the packet.
+ - Done either from an AggMAC field (if this is a flyover), or directly from the unmodified packet.
+ - If the computed MAC is not equal to that of the packet, drop the packet.
+1. If the packet was marked as guaranteed forwarding:
+ 1. Check the bandwidth usage of its reservation. If bucket is full, mark the packet as best-effort.
+1. Check the computed guarantee class of the packet:
+ 1. If it is guaranteed forwarding, forward it with priority.
+ 1. Otherwise, forward it as best effort.
+
+
+### Authentication Key ($A_k$) Computation
+The first step to compute the flyover MAC is to obtain the authentication key $A_k$.
+This is done by creating a 16-byte input buffer (block) and encrypting it with AES-128,
+initialized with the Hummingbird secret value (analogous to the regular SCION master secret).
+This input buffer is created as follows:
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ConsIngress | ConsEgress |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ResID | BW |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ResStart |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| ResDuration | 0 Padding |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+
+One block of AES derives the authentication key. Example in Go:
+```go
+inputBuff := prepareBuffer(in, eg, resID, bw, restStart, resDuration)
+block, _ = aes.NewCipher(secretValue)
+block.Encrypt(inputBuff, inputBuff)
+authenticationKey := inputBuff
+```
+
+### FlyoverMAC Computation
+Analogous to the authentication key computation, the flyover MAC computation
+is done by a block encryption with AES-128,
+initialized with the authentication key $A_k$, on a 16-byte input buffer.
+The input buffer layout is depicted below:
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| DstISD | |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+| DstAS |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| PktLen | ResStartOffset |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| MillisTimestamp | Counter |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+Then the FlyoverMAC is computed with one block of AES:
+
+```go
+inputBuff := prepareBuffer(dstISD, dstAS, pktLen, restStartOffset, millisTimestamp, counter)
+block, _ = aes.NewCipher(authenticationKey)
+block.Encrypt(inputBuff, inputBuff)
+flyoverMAC := inputBuff
+```
+
+### Bandwidth Policing
+The bandwidth policing is done by means of a token bucket algorithm,
+using 8 bytes to keep a timestamp per reservation ID.
+These timestamps can be stored in a global *Timestamps* array,
+allowing for an efficient space usage,
+provided that the reservation IDs present in the packets are sequential,
+which we control.
+
+```go
+// Returns true if forwarding with priority.
+func BandwidthPolice(pkt, Timestamps) bool
+ now := time.Now()
+ TS := max(Timestamps[pkt.ResID], now) + (pkt.Len / pkt.BW)
+ if TS <= now + BURST_TIME {
+ Timestamps[pkt.ResID] = TS
+ return true
+ }
+ return false
+```
+
+
+
+## Implementation
+The implementation requires changes in the border router, namely:
+- The border router must support priority queues. Optionally, priority processing of packets.
+- There must be a new `Hummingbird` path type, allowing its (de)serialization.
+
+The requirement to support priorities by the border router can be relaxed to only support
+priority egress queues, if we assume that forwarding packets at the border router is not bottlenecked
+by the processing of the packets.
+
+### Questions
+I find two main questions to finish a design to the implementation:
+
+1. A border router with priorities is needed. Will it be enough to add priority queues like proposed on PR #4054 ?
+2. The processing of a Hummingbird packet and a SCION one is very similar, but the functions _operate_ on different path types. A decision on how to avoid duplicating code must be made.
+
+### Avoiding code duplication
+Generics in Go do not allow the definition of a generic function that uses a generic type as a receiver of
+a function, unless the type is constrained to an interface. The following is not allowed:
+```go
+func processPeer[T any](p *T) (bool, disposition) {
+ peer, err := determinePeer(p.PathMeta, p.InfoFields[p.CurrInf])
+ if err != nil {
+ return peer, errorDiscard("error", err)
+ }
+ return peer, pForward
+}
+```
+The following IS allowed:
+```go
+type pathLike interface {
+ PathMeta() scion.MetaHdr
+ Infofield(int) path.InfoField
+ CurrInf() int
+}
+
+func processPeer[T any](p pathLike) (bool, disposition) {
+ peer, err := determinePeer(p.PathMeta(), p.Infofield(p.CurrInf()))
+ if err != nil {
+ return peer, errorDiscard("error", err)
+ }
+ return peer, pForward
+}
+```
+
+This fact prevents us from simply writing a generic function with the correct _text_ inside
+(namely accessing the struct's fields and functions) because the compiler won't allow it:
+an interface defining the common behavior of all paths must be defined.
+
+Evenmore, Go does not support template specialization with its generics.
+This means that we can't modified a _default_ behavior (e.g. SCION packet processing) with the definition
+of a function only for a specialized type (e.g. a Hummingbird path type).
+Possibly, the only advantage of using generics here would be easing the task of the compiler to remove
+type assertions when using the interface.
+The Go compiler has proven to be very efficient in removing this superfluous type assertion code
+in the presence of only one type in the call stack, although this would be true only when all functions are
+inlined.
+
+With the use of generics we would be forcing the compiler to effectively create distinct definitions
+for the distinct path types, which is an assurance that the superfluous type assertion won't be there.
+
+The need to create a common interface to express the processing of all path types is still present though.
+
+
+### Processing a Packet
+The processing of a packet with certain (segment-based) path type looks like this:
+```go
+// Pseudo-code:
+func parsePath() disposition
+func validate() disposition // expiry, ingress id, pkt length, transit underlay, src&dst&host.
+func verifyMAC() disposition
+func updateSegID() disposition
+func determinePeer() disposition
+func ingressRouterAlert() disposition
+func goInbound() disposition // terminal state: local IA delivery.
+func doXover() disposition // already present in the path like object?
+func validateEgressID() disposition
+func egressRouterAlert() disposition
+func validateEgressRouterUp() disposition
+```
+These functions on the packet processor would be written once, dealing with a generic path type.
+As an example we can use the `processPeer` function depicted above.
+
+Following this approach, the path types declared a the `slayers` level would either have to support this
+`pathLike` interface, or we would create _wrappers_ around them to support the interface.
+
+
+### Summary
+Regarding the implementation, it can be desined as follows:
+- The border router will support priority queues.
+ A new field `trafficClass` inside the `Packet` struct
+can be declared to support priorities.
+- The processing of a packet is written in one set of functions:
+ the ones we have in `dataplane.go` to process a SCION packet.
+ These functions will operate on an interface (aka `PathLike` or similar).
+- The path types from `slayers` will be wrapped in a struct each to support the `PathLike` interface,
+ or themselves support the `PathLike` interface.
diff --git a/doc/dev/design/Hummingbird.rst b/doc/dev/design/Hummingbird.rst
new file mode 100644
index 0000000000..e519875dce
--- /dev/null
+++ b/doc/dev/design/Hummingbird.rst
@@ -0,0 +1,324 @@
+************************
+Hummingbird QoS Protocol
+************************
+
+- Author: Juan A. Garcia-Pardo
+- Last updated: 2025-11-12
+- Discussion at: -
+
+Abstract
+========
+Hummingbird is a QoS protocol that runs on top of SCION.
+It allows ASes to list part of their bandwidth in marketplaces,
+and clients to purchase this bandwidth via reservations.
+The protocol consists of three distinct planes:
+
+- Control-plane.
+- Marketplace.
+- Data-plane.
+
+The control-plane is inherited from the SCION control-plane,
+and allows ASes and clients to find paths between source and destination pairs.
+Additionally, ASes can create *flyovers* between an ingress and an egress interface,
+with certain bandwidth and validity time (start-time and end-time of the flyover).
+These flyovers are then listed in a marketplace in the form of *assets*,
+and made available for clients to purchase.
+
+At the marketplace, when a client purchases an asset,
+it has the right to sell it again, or to convert it into a reservation.
+A reservation is created from an asset via a process of *redemption*,
+where the client contacts the owning AS with the asset and the client information,
+morphing the asset into an intransferrable reservation,
+valid only for that particular client.
+Reservations are not tradeable anymore, as they work only for the client that redeemed the asset.
+
+Once the reservation is created,
+it can be used on a path that transits the owning AS using the ingress-egress pair of the reservation.
+If the reservation is used following the specified parameters of bandwidth and validity time,
+the packets transiting that AS are guaranteed to be forwarded without losses.
+
+.. figure:: fig/Hummingbird/one-flyover.png
+
+ This path between a source end-host in AS A, and a destination end-host in AS D,
+ contains only one flyover, guaranteed transit at AS B through interfaces 1馃2,
+ with bandwidth 3Mbps, and valid between 13:20 and 13:40 (full timestamp not shown for
+ brevity).
+ Note that the transit through AS C does not have a reservation, thus packets may be dropped there.
+
+The interactions with the marketplace is out of scope of both this document
+and the implementation of Hummingbird for SCION in scionproto.
+This interaction includes the following:
+
+- Publishing assets into the marketplace.
+- Purchasing assets from the marketplace.
+- Redemption of assets into reservations.
+
+
+Control-plane
+=============
+Once the reservations are obtained by the client,
+they can be assembled into existing paths to create a fully or partially reserved path:
+fully when all hops contain reservations, partially reserved when not.
+
+Obtaining paths from source to destination is done by means of the SCION control-plane,
+and it is typically done by the clients before obtaining the reservations.
+Once a regular SCION path exists, and the reservations are obtained,
+there is a process of merging both together.
+This yields a reserved path, that is usable by the data-plane.
+
+
+Data-plane
+==========
+The data-plane in Hummingbird is organized very similarly to the regular SCION case.
+Border routers do not keep state for forwarding other than one secret key.
+This key is rotated in a similar way than the master secret is rotated in the regular SCION case.
+
+The rest of this section is organized as follows:
+
+- Description of the wire format of Hummingbird packets.
+- Processing steps of the border router when forwarding a Hummingbird packet.
+
+Wire Format
+-----------
+
+A packet using the Hummingbird protocol is similar in structure to a regular SCION one,
+and contains the following:
+
+- A path metaheader.
+- Between 1 and 3 info fields.
+- Between 1 and 64 flyover or hop fields.
+
+
+
+
+Metaheader
+^^^^^^^^^^
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | C | CurrHF |r| Seg0Len | Seg1Len | Seg2Len |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | BaseTimeStamp |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | MillisTimestamp | Counter |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+- **(C)urrINF**: 2-bit index (0-based) pointing to the current info field (see offset calculations below).
+- **CurrHF (changed)**: 8-bit index (0-based) pointing to the start of the current hop field (see offset calculations below) in 4-byte increments. This index is increased by 3 for normal hop fields and by 5 for flyover hop fields, which are 12 B and 20 B long, respectively.
+- **r**: Unused and reserved for future use.
+- **Seg{0,1,2}Len (changed)**: 7-bit encoding of the length of each segment. The value in these fields is the length of the respective segment in bytes divided by 4. Seg[i]Len > 0 implies the existence of info field i. If a given Seg[i]Len is zero, all subsequent Seg[j]Len with j > i will also be zero.
+- **BaseTimestamp (new)**: A unix timestamp (unsigned integer, 1-second granularity, similar to beacon timestamp in normal SCION path segments) that is used as a base to calculate start times for flyovers and the high granularity MillisTimestamp.
+- **MillisTimestamp (new)**: Millisecond granularity timestamp, as offset from BaseTimestamp. Used to compute MACs for flyover hops and to check recentness of a packet.
+- **Counter (new)**: A counter for each packet that is sent by the source to ensure that the tuple (BaseTimestamp, MillisTimestamp, Counter) is unique. This can then be used for the optional duplicate suppression at an AS.
+
+
+The number of info fields present in the path is calculated as the amount of
+Seg[i]Len that are greater than zero, based on the following:
+
+.. code-block::
+
+ function InfFieldCount():
+ if Seg2Len != 0:
+ return 3
+ else if Seg1Len != 0:
+ return 2
+ else if Seg0Len != 0:
+ return 1
+ else
+ // All fields are zero. Intra-AS path.
+ return 0
+
+ NumInf = InfFieldCount()
+
+
+The path offsets are computed as:
+
+- InfoFieldOffset = 12 B + 8 B 路 CurrINF
+- HopFieldOffset = 12 B + 8 B 路 NumINF + 4 B 路 CurrHF
+
+
+InfoField
+^^^^^^^^^
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |r r r r r r P C| RSV | SegID |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | TimeStamp |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+- **r**: Unused and reserved for future use.
+- **P**: Peering flag. If set to true, then the forwarding path is built as a peering path, which requires special processing on the data plane.
+- **C**: Construction direction flag. If set to true then the hop fields are arranged in the direction they have been constructed during beaconing.
+- **RSV**: Unused and reserved for future use.
+- **SegID**: Updatable field used in the MAC-chaining mechanism.
+- **Timestamp**: Timestamp created by the initiator of the corresponding beacon. The timestamp is expressed in Unix time, and is encoded as an unsigned integer within 4 bytes with 1-second time granularity. It enables validation of the hop field by verification of the expiration time and MAC.
+
+
+Regular HopField
+^^^^^^^^^^^^^^^^
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |F r r r r r I E| ExpTime | ConsIngress |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ConsEgress | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+ | HopFieldMAC |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+- **F (new)**: Flyover bit. Indicates whether this is a hop field or a flyover hop field. Set to 0 for HopFields.
+- **r (unchanged)**: Unused and reserved for future use.
+- **I (unchanged)**: ConsIngress Router Alert. If the ConsIngress Router Alert is set, the ingress router (in construction direction) will process the L4 payload in the packet.
+- **E (unchanged)**: ConsEgress Router Alert. If the ConsEgress Router Alert is set, the egress router (in construction direction) will process the L4 payload in the packet. ExpTime (unchanged) Expiry time of a hop field. The field is 1-byte long, thus there are 256 different values available to express an expiration time. The expiration time expressed by the value of this field is relative, and an absolute expiration time in seconds is computed in combination with the timestamp field (from the corresponding info field).
+- **ConsIngress, ConsEgress (unchanged)**: The 16-bit interface IDs in construction direction.
+- **HopFieldMAC (name changed)**: 6-byte MAC to authenticate the hop field. For details on how this MAC is calculated refer to the hop-field MAC computation of the SCION path type.
+
+
+FlyoverHopField
+^^^^^^^^^^^^^^^
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |F r r r r r I E| ExpTime | ConsIngress |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ConsEgress | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+ | AggMAC |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ResID | BW |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ResStartOffset | ResDuration |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+- **F Flyover bit**: Indicates whether this is a hop field or a flyover hop field. Set to 1 for FlyoverHopFields.
+- **r, I, E, ExpTime, ConsIngress, ConsEgress**: These values are the same as in the standard HopField. Note that ExpTime is the expiration time of the standard HopField, not the expiration time of the reservation.
+- **AggMAC**: See below.
+- **ResID**: 22-bit Reservation ID, this allows for approximately 4 million concurrent reservations for a given ingress/egress pair.
+- **BW**: 10-bit bandwidth field indicating the reserved bandwidth which allows for 1024 different values. The values could be encoded similarly to floating point numbers (but without negative numbers or fractions), where some bits encode the exponent and some the significant digits. For example, one can use 5 bits for the exponent and 5 bits for the significand and calculate the value as significand if exponent = 0 or (32 + significand) << (exponent - 1) otherwise. This allows values from 0 to almost :math:`2^36` with an even spacing for each interval between powers of 2.
+- **ResStartOffset**: The offset between the BaseTimestamp in the Path Meta header and the start of the reservation (in seconds). This allows values up to approximately 18 hours in second granularity.
+- **ResDuration**: Duration of the reservation, i.e., the difference between the timestamps of the start and expiration time of the reservation.
+
+
+Forwarding Steps for a Hummingbird packet
+-----------------------------------------
+When a packet arrives at the ingress queue of a border router,
+the first step after dequeuing it is to determine its path type.
+If the path type is Hummingbird, the border router can determine the following actions:
+
+- Forward it with priority (reserved transit).
+- Forward it as best effort.
+- Drop the packet.
+
+The following steps are used to determine each action.
+During processing, if an inconsistency is found in the packet (declared sizes overflowing the packet, etc.),
+the packet is dropped. (left out of the sequence for clarity)
+
+#. If this hop is marked as a flyover:
+ #. Compute the FlyoverMAC. The FlyoverMAC is used to further compute the aggregated MAC (AggMAC).
+ #. If the reserved flyover is no longer valid, the packet will be best-effort.
+ #. The packet is marked internally as guaranteed forwarding.
+#. Compute the regular SCION hop field MAC (HopFieldMAC) and compare with that of the packet.
+ - Done either from an AggMAC field (if this is a flyover), or directly from the unmodified packet.
+ - If the computed MAC is not equal to that of the packet, drop the packet.
+#. If the packet was marked as guaranteed forwarding:
+ #. Check the bandwidth usage of its reservation. If bucket is full, mark the packet as best-effort.
+#. Check the computed guarantee class of the packet:
+ #. If it is guaranteed forwarding, forward it with priority.
+ #. Otherwise, forward it as best effort.
+
+
+Authentication Key (:math:`A_k`) Computation
+--------------------------------------------
+The first step to compute the flyover MAC is to obtain the authentication key :math:`A_k`.
+This is done by creating a 16-byte input buffer (block) and encrypting it with AES-128,
+initialized with the Hummingbird secret value (analogous to the regular SCION master secret).
+This input buffer is created as follows:
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ConsIngress | ConsEgress |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ResID | BW |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ResStart |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | ResDuration | 0 Padding |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+One block of AES derives the authentication key. Example in Go:
+
+.. code-block:: Go
+
+ inputBuff := prepareBuffer(in, eg, resID, bw, restStart, resDuration)
+ block, _ = aes.NewCipher(secretValue)
+ block.Encrypt(inputBuff, inputBuff)
+ authenticationKey := inputBuff
+
+
+FlyoverMAC Computation
+----------------------
+Analogous to the authentication key computation, the flyover MAC computation
+is done by a block encryption with AES-128,
+initialized with the authentication key :math:`A_k`, on a 16-byte input buffer.
+The input buffer layout is depicted below:
+
+.. code-block::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | DstISD | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
+ | DstAS |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | PktLen | ResStartOffset |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | MillisTimestamp | Counter |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+Then the FlyoverMAC is computed with one block of AES:
+
+.. code-block:: Go
+
+ inputBuff := prepareBuffer(dstISD, dstAS, pktLen, restStartOffset, millisTimestamp, counter)
+ block, _ = aes.NewCipher(authenticationKey)
+ block.Encrypt(inputBuff, inputBuff)
+ flyoverMAC := inputBuff
+
+
+Bandwidth Policing
+------------------
+The bandwidth policing is done by means of a token bucket algorithm,
+using 8 bytes to keep a timestamp per reservation ID.
+These timestamps can be stored in a global *Timestamps* array,
+allowing for an efficient space usage,
+provided that the reservation IDs present in the packets are sequential,
+which we control.
+
+.. code-block:: Go
+
+ // Returns true if forwarding with priority.
+ func BandwidthPolice(pkt, Timestamps) bool
+ now := time.Now()
+ TS := max(Timestamps[pkt.ResID], now) + (pkt.Len / pkt.BW)
+ if TS <= now + BURST_TIME {
+ Timestamps[pkt.ResID] = TS
+ return true
+ }
+ return false
diff --git a/doc/dev/design/fig/Hummingbird/one-flyover.png b/doc/dev/design/fig/Hummingbird/one-flyover.png
new file mode 100644
index 0000000000..5f48491f21
Binary files /dev/null and b/doc/dev/design/fig/Hummingbird/one-flyover.png differ
diff --git a/pkg/slayers/BUILD.bazel b/pkg/slayers/BUILD.bazel
index 00092a8641..1fcd29cad2 100644
--- a/pkg/slayers/BUILD.bazel
+++ b/pkg/slayers/BUILD.bazel
@@ -23,6 +23,7 @@ go_library(
"//pkg/slayers/path:go_default_library",
"//pkg/slayers/path/empty:go_default_library",
"//pkg/slayers/path/epic:go_default_library",
+ "//pkg/slayers/path/hummingbird:go_default_library",
"//pkg/slayers/path/onehop:go_default_library",
"//pkg/slayers/path/scion:go_default_library",
"@com_github_gopacket_gopacket//:go_default_library",
diff --git a/pkg/slayers/path/hummingbird/BUILD.bazel b/pkg/slayers/path/hummingbird/BUILD.bazel
new file mode 100644
index 0000000000..1e55e86177
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/BUILD.bazel
@@ -0,0 +1,58 @@
+load("@rules_go//go:def.bzl", "go_library")
+load("//tools:go.bzl", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "asm_amd64.s",
+ "asm_arm64.s",
+ "asm_ppc64x.s",
+ "base.go",
+ "decoded.go",
+ "flyoverhopfield.go",
+ "mac.go",
+ "raw.go",
+ ],
+ importpath = "github.com/scionproto/scion/pkg/slayers/path/hummingbird",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/slayers/path:go_default_library",
+ "//pkg/slayers/path/scion:go_default_library",
+ ] + select({
+ "@rules_go//go/platform:amd64": [
+ "//pkg/addr:go_default_library",
+ ],
+ "@rules_go//go/platform:arm64": [
+ "//pkg/addr:go_default_library",
+ ],
+ "@rules_go//go/platform:ppc64": [
+ "//pkg/addr:go_default_library",
+ ],
+ "@rules_go//go/platform:ppc64le": [
+ "//pkg/addr:go_default_library",
+ ],
+ "//conditions:default": [],
+ }),
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = [
+ "base_test.go",
+ "decoded_test.go",
+ "export_test.go",
+ "flyoverhopfield_test.go",
+ "mac_test.go",
+ "raw_test.go",
+ "testpaths_test.go",
+ ],
+ embed = [":go_default_library"],
+ deps = [
+ "//pkg/addr:go_default_library",
+ "//pkg/slayers/path:go_default_library",
+ "//pkg/slayers/path/scion:go_default_library",
+ "@com_github_stretchr_testify//assert:go_default_library",
+ "@com_github_stretchr_testify//require:go_default_library",
+ ],
+)
diff --git a/pkg/slayers/path/hummingbird/asm_amd64.s b/pkg/slayers/path/hummingbird/asm_amd64.s
new file mode 100644
index 0000000000..12bde89cd4
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/asm_amd64.s
@@ -0,0 +1,274 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is mostly a copy of the file of the same name in the crypto/aes go package
+// The key expansion for the decryption keys has been removed in this file
+
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "textflag.h"
+
+// func encryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路encryptBlockAsm(SB),NOSPLIT,$0
+ MOVQ nr+0(FP), CX
+ MOVQ xk+8(FP), AX
+ MOVQ dst+16(FP), DX
+ MOVQ src+24(FP), BX
+ MOVUPS 0(AX), X1
+ MOVUPS 0(BX), X0
+ ADDQ $16, AX
+ PXOR X1, X0
+ SUBQ $12, CX
+ JE Lenc192
+ JB Lenc128
+Lenc256:
+ MOVUPS 0(AX), X1
+ AESENC X1, X0
+ MOVUPS 16(AX), X1
+ AESENC X1, X0
+ ADDQ $32, AX
+Lenc192:
+ MOVUPS 0(AX), X1
+ AESENC X1, X0
+ MOVUPS 16(AX), X1
+ AESENC X1, X0
+ ADDQ $32, AX
+Lenc128:
+ MOVUPS 0(AX), X1
+ AESENC X1, X0
+ MOVUPS 16(AX), X1
+ AESENC X1, X0
+ MOVUPS 32(AX), X1
+ AESENC X1, X0
+ MOVUPS 48(AX), X1
+ AESENC X1, X0
+ MOVUPS 64(AX), X1
+ AESENC X1, X0
+ MOVUPS 80(AX), X1
+ AESENC X1, X0
+ MOVUPS 96(AX), X1
+ AESENC X1, X0
+ MOVUPS 112(AX), X1
+ AESENC X1, X0
+ MOVUPS 128(AX), X1
+ AESENC X1, X0
+ MOVUPS 144(AX), X1
+ AESENCLAST X1, X0
+ MOVUPS X0, 0(DX)
+ RET
+
+// func decryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路decryptBlockAsm(SB),NOSPLIT,$0
+ MOVQ nr+0(FP), CX
+ MOVQ xk+8(FP), AX
+ MOVQ dst+16(FP), DX
+ MOVQ src+24(FP), BX
+ MOVUPS 0(AX), X1
+ MOVUPS 0(BX), X0
+ ADDQ $16, AX
+ PXOR X1, X0
+ SUBQ $12, CX
+ JE Ldec192
+ JB Ldec128
+Ldec256:
+ MOVUPS 0(AX), X1
+ AESDEC X1, X0
+ MOVUPS 16(AX), X1
+ AESDEC X1, X0
+ ADDQ $32, AX
+Ldec192:
+ MOVUPS 0(AX), X1
+ AESDEC X1, X0
+ MOVUPS 16(AX), X1
+ AESDEC X1, X0
+ ADDQ $32, AX
+Ldec128:
+ MOVUPS 0(AX), X1
+ AESDEC X1, X0
+ MOVUPS 16(AX), X1
+ AESDEC X1, X0
+ MOVUPS 32(AX), X1
+ AESDEC X1, X0
+ MOVUPS 48(AX), X1
+ AESDEC X1, X0
+ MOVUPS 64(AX), X1
+ AESDEC X1, X0
+ MOVUPS 80(AX), X1
+ AESDEC X1, X0
+ MOVUPS 96(AX), X1
+ AESDEC X1, X0
+ MOVUPS 112(AX), X1
+ AESDEC X1, X0
+ MOVUPS 128(AX), X1
+ AESDEC X1, X0
+ MOVUPS 144(AX), X1
+ AESDECLAST X1, X0
+ MOVUPS X0, 0(DX)
+ RET
+
+// func expandKeyAsm(nr int, key *byte, enc) {
+// Note that round keys are stored in uint128 format, not uint32
+TEXT 路expandKeyAsm(SB),NOSPLIT,$0
+ MOVQ nr+0(FP), CX
+ MOVQ key+8(FP), AX
+ MOVQ enc+16(FP), BX
+ MOVUPS (AX), X0
+ // enc
+ MOVUPS X0, (BX)
+ ADDQ $16, BX
+ PXOR X4, X4 // _expand_key_* expect X4 to be zero
+ CMPL CX, $12
+ JE Lexp_enc192
+ JB Lexp_enc128
+Lexp_enc256:
+ MOVUPS 16(AX), X2
+ MOVUPS X2, (BX)
+ ADDQ $16, BX
+ AESKEYGENASSIST $0x01, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x01, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x02, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x02, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x04, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x04, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x08, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x08, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x10, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x10, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x20, X2, X1
+ CALL _expand_key_256a<>(SB)
+ AESKEYGENASSIST $0x20, X0, X1
+ CALL _expand_key_256b<>(SB)
+ AESKEYGENASSIST $0x40, X2, X1
+ CALL _expand_key_256a<>(SB)
+ RET
+Lexp_enc192:
+ MOVQ 16(AX), X2
+ AESKEYGENASSIST $0x01, X2, X1
+ CALL _expand_key_192a<>(SB)
+ AESKEYGENASSIST $0x02, X2, X1
+ CALL _expand_key_192b<>(SB)
+ AESKEYGENASSIST $0x04, X2, X1
+ CALL _expand_key_192a<>(SB)
+ AESKEYGENASSIST $0x08, X2, X1
+ CALL _expand_key_192b<>(SB)
+ AESKEYGENASSIST $0x10, X2, X1
+ CALL _expand_key_192a<>(SB)
+ AESKEYGENASSIST $0x20, X2, X1
+ CALL _expand_key_192b<>(SB)
+ AESKEYGENASSIST $0x40, X2, X1
+ CALL _expand_key_192a<>(SB)
+ AESKEYGENASSIST $0x80, X2, X1
+ CALL _expand_key_192b<>(SB)
+ RET
+Lexp_enc128:
+ AESKEYGENASSIST $0x01, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x02, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x04, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x08, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x10, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x20, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x40, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x80, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x1b, X0, X1
+ CALL _expand_key_128<>(SB)
+ AESKEYGENASSIST $0x36, X0, X1
+ CALL _expand_key_128<>(SB)
+ RET
+
+TEXT _expand_key_128<>(SB),NOSPLIT,$0
+ PSHUFD $0xff, X1, X1
+ SHUFPS $0x10, X0, X4
+ PXOR X4, X0
+ SHUFPS $0x8c, X0, X4
+ PXOR X4, X0
+ PXOR X1, X0
+ MOVUPS X0, (BX)
+ ADDQ $16, BX
+ RET
+
+TEXT _expand_key_192a<>(SB),NOSPLIT,$0
+ PSHUFD $0x55, X1, X1
+ SHUFPS $0x10, X0, X4
+ PXOR X4, X0
+ SHUFPS $0x8c, X0, X4
+ PXOR X4, X0
+ PXOR X1, X0
+
+ MOVAPS X2, X5
+ MOVAPS X2, X6
+ PSLLDQ $0x4, X5
+ PSHUFD $0xff, X0, X3
+ PXOR X3, X2
+ PXOR X5, X2
+
+ MOVAPS X0, X1
+ SHUFPS $0x44, X0, X6
+ MOVUPS X6, (BX)
+ SHUFPS $0x4e, X2, X1
+ MOVUPS X1, 16(BX)
+ ADDQ $32, BX
+ RET
+
+TEXT _expand_key_192b<>(SB),NOSPLIT,$0
+ PSHUFD $0x55, X1, X1
+ SHUFPS $0x10, X0, X4
+ PXOR X4, X0
+ SHUFPS $0x8c, X0, X4
+ PXOR X4, X0
+ PXOR X1, X0
+
+ MOVAPS X2, X5
+ PSLLDQ $0x4, X5
+ PSHUFD $0xff, X0, X3
+ PXOR X3, X2
+ PXOR X5, X2
+
+ MOVUPS X0, (BX)
+ ADDQ $16, BX
+ RET
+
+TEXT _expand_key_256a<>(SB),NOSPLIT,$0
+ JMP _expand_key_128<>(SB)
+
+TEXT _expand_key_256b<>(SB),NOSPLIT,$0
+ PSHUFD $0xaa, X1, X1
+ SHUFPS $0x10, X2, X4
+ PXOR X4, X2
+ SHUFPS $0x8c, X2, X4
+ PXOR X4, X2
+ PXOR X1, X2
+
+ MOVUPS X2, (BX)
+ ADDQ $16, BX
+ RET
diff --git a/pkg/slayers/path/hummingbird/asm_arm64.s b/pkg/slayers/path/hummingbird/asm_arm64.s
new file mode 100644
index 0000000000..e500dfaae2
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/asm_arm64.s
@@ -0,0 +1,231 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is mostly a copy of the file of the same name in the crypto/aes go package
+// The key expansion for the decryption keys has been removed in this file
+
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "textflag.h"
+DATA rotInvSRows<>+0x00(SB)/8, $0x080f0205040b0e01
+DATA rotInvSRows<>+0x08(SB)/8, $0x00070a0d0c030609
+GLOBL rotInvSRows<>(SB), (NOPTR+RODATA), $16
+DATA invSRows<>+0x00(SB)/8, $0x0b0e0104070a0d00
+DATA invSRows<>+0x08(SB)/8, $0x0306090c0f020508
+GLOBL invSRows<>(SB), (NOPTR+RODATA), $16
+// func encryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路encryptBlockAsm(SB),NOSPLIT,$0
+ MOVD nr+0(FP), R9
+ MOVD xk+8(FP), R10
+ MOVD dst+16(FP), R11
+ MOVD src+24(FP), R12
+
+ VLD1 (R12), [V0.B16]
+
+ CMP $12, R9
+ BLT enc128
+ BEQ enc196
+enc256:
+ VLD1.P 32(R10), [V1.B16, V2.B16]
+ AESE V1.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V2.B16, V0.B16
+ AESMC V0.B16, V0.B16
+enc196:
+ VLD1.P 32(R10), [V3.B16, V4.B16]
+ AESE V3.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V4.B16, V0.B16
+ AESMC V0.B16, V0.B16
+enc128:
+ VLD1.P 64(R10), [V5.B16, V6.B16, V7.B16, V8.B16]
+ VLD1.P 64(R10), [V9.B16, V10.B16, V11.B16, V12.B16]
+ VLD1.P 48(R10), [V13.B16, V14.B16, V15.B16]
+ AESE V5.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V6.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V7.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V8.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V9.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V10.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V11.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V12.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V13.B16, V0.B16
+ AESMC V0.B16, V0.B16
+ AESE V14.B16, V0.B16
+ VEOR V0.B16, V15.B16, V0.B16
+ VST1 [V0.B16], (R11)
+ RET
+
+// func decryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路decryptBlockAsm(SB),NOSPLIT,$0
+ MOVD nr+0(FP), R9
+ MOVD xk+8(FP), R10
+ MOVD dst+16(FP), R11
+ MOVD src+24(FP), R12
+
+ VLD1 (R12), [V0.B16]
+
+ CMP $12, R9
+ BLT dec128
+ BEQ dec196
+dec256:
+ VLD1.P 32(R10), [V1.B16, V2.B16]
+ AESD V1.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V2.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+dec196:
+ VLD1.P 32(R10), [V3.B16, V4.B16]
+ AESD V3.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V4.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+dec128:
+ VLD1.P 64(R10), [V5.B16, V6.B16, V7.B16, V8.B16]
+ VLD1.P 64(R10), [V9.B16, V10.B16, V11.B16, V12.B16]
+ VLD1.P 48(R10), [V13.B16, V14.B16, V15.B16]
+ AESD V5.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V6.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V7.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V8.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V9.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V10.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V11.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V12.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V13.B16, V0.B16
+ AESIMC V0.B16, V0.B16
+ AESD V14.B16, V0.B16
+ VEOR V0.B16, V15.B16, V0.B16
+ VST1 [V0.B16], (R11)
+ RET
+
+// func expandKeyAsm(nr int, key *byte, enc) {
+// Note that round keys are stored in uint128 format, not uint32
+TEXT 路expandKeyAsm(SB),NOSPLIT,$0
+ MOVD nr+0(FP), R8
+ MOVD key+8(FP), R9
+ MOVD enc+16(FP), R10
+ LDP rotInvSRows<>(SB), (R0, R1)
+ VMOV R0, V3.D[0]
+ VMOV R1, V3.D[1]
+ VEOR V0.B16, V0.B16, V0.B16 // All zeroes
+ MOVW $1, R13
+ TBZ $1, R8, ks192
+ TBNZ $2, R8, ks256
+ LDPW (R9), (R4, R5)
+ LDPW 8(R9), (R6, R7)
+ STPW.P (R4, R5), 8(R10)
+ STPW.P (R6, R7), 8(R10)
+ MOVW $0x1b, R14
+ks128Loop:
+ VMOV R7, V2.S[0]
+ WORD $0x4E030042 // TBL V3.B16, [V2.B16], V2.B16
+ AESE V0.B16, V2.B16 // Use AES to compute the SBOX
+ EORW R13, R4
+ LSLW $1, R13 // Compute next Rcon
+ ANDSW $0x100, R13, ZR
+ CSELW NE, R14, R13, R13 // Fake modulo
+ SUBS $1, R8
+ VMOV V2.S[0], R0
+ EORW R0, R4
+ EORW R4, R5
+ EORW R5, R6
+ EORW R6, R7
+ STPW.P (R4, R5), 8(R10)
+ STPW.P (R6, R7), 8(R10)
+ BNE ks128Loop
+ B ksDone // If dec is nil we are done
+ks192:
+ LDPW (R9), (R2, R3)
+ LDPW 8(R9), (R4, R5)
+ LDPW 16(R9), (R6, R7)
+ STPW.P (R2, R3), 8(R10)
+ STPW.P (R4, R5), 8(R10)
+ SUB $4, R8
+ks192Loop:
+ STPW.P (R6, R7), 8(R10)
+ VMOV R7, V2.S[0]
+ WORD $0x4E030042 //TBL V3.B16, [V2.B16], V2.B16
+ AESE V0.B16, V2.B16
+ EORW R13, R2
+ LSLW $1, R13
+ SUBS $1, R8
+ VMOV V2.S[0], R0
+ EORW R0, R2
+ EORW R2, R3
+ EORW R3, R4
+ EORW R4, R5
+ EORW R5, R6
+ EORW R6, R7
+ STPW.P (R2, R3), 8(R10)
+ STPW.P (R4, R5), 8(R10)
+ BNE ks192Loop
+ B ksDone
+ks256:
+ LDP invSRows<>(SB), (R0, R1)
+ VMOV R0, V4.D[0]
+ VMOV R1, V4.D[1]
+ LDPW (R9), (R0, R1)
+ LDPW 8(R9), (R2, R3)
+ LDPW 16(R9), (R4, R5)
+ LDPW 24(R9), (R6, R7)
+ STPW.P (R0, R1), 8(R10)
+ STPW.P (R2, R3), 8(R10)
+ SUB $7, R8
+ks256Loop:
+ STPW.P (R4, R5), 8(R10)
+ STPW.P (R6, R7), 8(R10)
+ VMOV R7, V2.S[0]
+ WORD $0x4E030042 //TBL V3.B16, [V2.B16], V2.B16
+ AESE V0.B16, V2.B16
+ EORW R13, R0
+ LSLW $1, R13
+ SUBS $1, R8
+ VMOV V2.S[0], R9
+ EORW R9, R0
+ EORW R0, R1
+ EORW R1, R2
+ EORW R2, R3
+ VMOV R3, V2.S[0]
+ WORD $0x4E040042 //TBL V3.B16, [V2.B16], V2.B16
+ AESE V0.B16, V2.B16
+ VMOV V2.S[0], R9
+ EORW R9, R4
+ EORW R4, R5
+ EORW R5, R6
+ EORW R6, R7
+ STPW.P (R0, R1), 8(R10)
+ STPW.P (R2, R3), 8(R10)
+ BNE ks256Loop
+ B ksDone
+ksDone:
+ RET
diff --git a/pkg/slayers/path/hummingbird/asm_ppc64x.s b/pkg/slayers/path/hummingbird/asm_ppc64x.s
new file mode 100644
index 0000000000..5fc59baeb1
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/asm_ppc64x.s
@@ -0,0 +1,671 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is mostly a copy of the file of the same name in the crypto/aes go package
+// The key expansion for the decryption keys has been removed in this file
+
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ppc64 || ppc64le
+
+// Based on CRYPTOGAMS code with the following comment:
+// # ====================================================================
+// # Written by Andy Polyakov for the OpenSSL
+// # project. The module is, however, dual licensed under OpenSSL and
+// # CRYPTOGAMS licenses depending on where you obtain it. For further
+// # details see http://www.openssl.org/~appro/cryptogams/.
+// # ====================================================================
+
+// Original code can be found at the link below:
+// https://github.com/dot-asm/cryptogams/blob/master/ppc/aesp8-ppc.pl
+
+// Some function names were changed to be consistent with Go function
+// names. For instance, function aes_p8_set_{en,de}crypt_key become
+// set{En,De}cryptKeyAsm. I also split setEncryptKeyAsm in two parts
+// and a new session was created (doEncryptKeyAsm). This was necessary to
+// avoid arguments overwriting when setDecryptKeyAsm calls setEncryptKeyAsm.
+// There were other modifications as well but kept the same functionality.
+
+#include "textflag.h"
+
+// For expandKeyAsm
+#define INP R3
+#define BITS R4
+#define OUTENC R5 // Pointer to next expanded encrypt key
+#define PTR R6
+#define CNT R7
+#define ROUNDS R8
+#define OUTDEC R9 // Pointer to next expanded decrypt key
+#define TEMP R19
+#define ZERO V0
+#define IN0 V1
+#define IN1 V2
+#define KEY V3
+#define RCON V4
+#define MASK V5
+#define TMP V6
+#define STAGE V7
+#define OUTPERM V8
+#define OUTMASK V9
+#define OUTHEAD V10
+#define OUTTAIL V11
+
+// For P9 instruction emulation
+#define ESPERM V21 // Endian swapping permute into BE
+#define TMP2 V22 // Temporary for P8_STXVB16X/P8_STXV
+
+// For {en,de}cryptBlockAsm
+#define BLK_INP R3
+#define BLK_OUT R4
+#define BLK_KEY R5
+#define BLK_ROUNDS R6
+#define BLK_IDX R7
+
+DATA 路rcon+0x00(SB)/8, $0x0f0e0d0c0b0a0908 // Permute for vector doubleword endian swap
+DATA 路rcon+0x08(SB)/8, $0x0706050403020100
+DATA 路rcon+0x10(SB)/8, $0x0100000001000000 // RCON
+DATA 路rcon+0x18(SB)/8, $0x0100000001000000 // RCON
+DATA 路rcon+0x20(SB)/8, $0x1b0000001b000000
+DATA 路rcon+0x28(SB)/8, $0x1b0000001b000000
+DATA 路rcon+0x30(SB)/8, $0x0d0e0f0c0d0e0f0c // MASK
+DATA 路rcon+0x38(SB)/8, $0x0d0e0f0c0d0e0f0c // MASK
+DATA 路rcon+0x40(SB)/8, $0x0000000000000000
+DATA 路rcon+0x48(SB)/8, $0x0000000000000000
+GLOBL 路rcon(SB), RODATA, $80
+
+// Emulate unaligned BE vector load/stores on LE targets
+#ifdef GOARCH_ppc64le
+#define P8_LXVB16X(RA,RB,VT) \
+ LXVD2X (RA+RB), VT \
+ VPERM VT, VT, ESPERM, VT
+
+#define P8_STXVB16X(VS,RA,RB) \
+ VPERM VS, VS, ESPERM, TMP2 \
+ STXVD2X TMP2, (RA+RB)
+
+#define LXSDX_BE(RA,RB,VT) \
+ LXSDX (RA+RB), VT \
+ VPERM VT, VT, ESPERM, VT
+#else
+#define P8_LXVB16X(RA,RB,VT) \
+ LXVD2X (RA+RB), VT
+
+#define P8_STXVB16X(VS,RA,RB) \
+ STXVD2X VS, (RA+RB)
+
+#define LXSDX_BE(RA,RB,VT) \
+ LXSDX (RA+RB), VT
+#endif
+
+// func setEncryptKeyAsm(nr int, key *byte, enc *uint32)
+TEXT 路expandKeyAsm(SB), NOSPLIT|NOFRAME, $0
+ // Load the arguments inside the registers
+ MOVD nr+0(FP), ROUNDS
+ MOVD key+8(FP), INP
+ MOVD enc+16(FP), OUTENC
+// MOVD dec+24(FP), OUTDEC
+
+#ifdef GOARCH_ppc64le
+ MOVD $路rcon(SB), PTR // PTR point to rcon addr
+ LVX (PTR), ESPERM
+ ADD $0x10, PTR
+#else
+ MOVD $路rcon+0x10(SB), PTR // PTR point to rcon addr (skipping permute vector)
+#endif
+
+ // Get key from memory and write aligned into VR
+ P8_LXVB16X(INP, R0, IN0)
+ ADD $0x10, INP, INP
+ MOVD $0x20, TEMP
+
+ CMPW ROUNDS, $12
+ LVX (PTR)(R0), RCON // lvx 4,0,6 Load first 16 bytes into RCON
+ LVX (PTR)(TEMP), MASK
+ ADD $0x10, PTR, PTR // addi 6,6,0x10 PTR to next 16 bytes of RCON
+ MOVD $8, CNT // li 7,8 CNT = 8
+ VXOR ZERO, ZERO, ZERO // vxor 0,0,0 Zero to be zero :)
+ MOVD CNT, CTR // mtctr 7 Set the counter to 8 (rounds)
+
+ // The expanded decrypt key is the expanded encrypt key stored in reverse order.
+ // Move OUTDEC to the last key location, and store in descending order.
+// ADD $160, OUTDEC, OUTDEC
+ BLT loop128
+// ADD $32, OUTDEC, OUTDEC
+ BEQ l192
+// ADD $32, OUTDEC, OUTDEC
+ JMP l256
+
+loop128:
+ // Key schedule (Round 1 to 8)
+ VPERM IN0, IN0, MASK, KEY // vperm 3,1,1,5 Rotate-n-splat
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VADDUWM RCON, RCON, RCON // vadduwm 4,4,4
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+ BC 0x10, 0, loop128 // bdnz .Loop128
+
+ LVX (PTR)(R0), RCON // lvx 4,0,6 Last two round keys
+
+ // Key schedule (Round 9)
+ VPERM IN0, IN0, MASK, KEY // vperm 3,1,1,5 Rotate-n-spat
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ // Key schedule (Round 10)
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VADDUWM RCON, RCON, RCON // vadduwm 4,4,4
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+
+ VPERM IN0, IN0, MASK, KEY // vperm 3,1,1,5 Rotate-n-splat
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ // Key schedule (Round 11)
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+
+ RET
+
+l192:
+ LXSDX_BE(INP, R0, IN1) // Load next 8 bytes into upper half of VSR in BE order.
+ MOVD $4, CNT // li 7,4
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+ VSPLTISB $8, KEY // vspltisb 3,8
+ MOVD CNT, CTR // mtctr 7
+ VSUBUBM MASK, KEY, MASK // vsububm 5,5,3
+
+loop192:
+ VPERM IN1, IN1, MASK, KEY // vperm 3,2,2,5
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+
+ VSLDOI $8, ZERO, IN1, STAGE // vsldoi 7,0,2,8
+ VSPLTW $3, IN0, TMP // vspltw 6,1,3
+ VXOR TMP, IN1, TMP // vxor 6,6,2
+ VSLDOI $12, ZERO, IN1, IN1 // vsldoi 2,0,2,12
+ VADDUWM RCON, RCON, RCON // vadduwm 4,4,4
+ VXOR IN1, TMP, IN1 // vxor 2,2,6
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+ VXOR IN1, KEY, IN1 // vxor 2,2,3
+ VSLDOI $8, STAGE, IN0, STAGE // vsldoi 7,7,1,8
+
+ VPERM IN1, IN1, MASK, KEY // vperm 3,2,2,5
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ STXVD2X STAGE, (R0+OUTENC)
+// STXVD2X STAGE, (R0+OUTDEC)
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ VSLDOI $8, IN0, IN1, STAGE // vsldoi 7,1,2,8
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ STXVD2X STAGE, (R0+OUTENC)
+// STXVD2X STAGE, (R0+OUTDEC)
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ VSPLTW $3, IN0, TMP // vspltw 6,1,3
+ VXOR TMP, IN1, TMP // vxor 6,6,2
+ VSLDOI $12, ZERO, IN1, IN1 // vsldoi 2,0,2,12
+ VADDUWM RCON, RCON, RCON // vadduwm 4,4,4
+ VXOR IN1, TMP, IN1 // vxor 2,2,6
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+ VXOR IN1, KEY, IN1 // vxor 2,2,3
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+ BC 0x10, 0, loop192 // bdnz .Loop192
+
+ RET
+
+l256:
+ P8_LXVB16X(INP, R0, IN1)
+ MOVD $7, CNT // li 7,7
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+ MOVD CNT, CTR // mtctr 7
+
+loop256:
+ VPERM IN1, IN1, MASK, KEY // vperm 3,2,2,5
+ VSLDOI $12, ZERO, IN0, TMP // vsldoi 6,0,1,12
+ STXVD2X IN1, (R0+OUTENC)
+// STXVD2X IN1, (R0+OUTDEC)
+ VCIPHERLAST KEY, RCON, KEY // vcipherlast 3,3,4
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN0, TMP, IN0 // vxor 1,1,6
+ VADDUWM RCON, RCON, RCON // vadduwm 4,4,4
+ VXOR IN0, KEY, IN0 // vxor 1,1,3
+ STXVD2X IN0, (R0+OUTENC)
+// STXVD2X IN0, (R0+OUTDEC)
+ ADD $16, OUTENC, OUTENC
+// ADD $-16, OUTDEC, OUTDEC
+ BC 0x12, 0, done // bdz .Ldone
+
+ VSPLTW $3, IN0, KEY // vspltw 3,1,3
+ VSLDOI $12, ZERO, IN1, TMP // vsldoi 6,0,2,12
+ VSBOX KEY, KEY // vsbox 3,3
+
+ VXOR IN1, TMP, IN1 // vxor 2,2,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN1, TMP, IN1 // vxor 2,2,6
+ VSLDOI $12, ZERO, TMP, TMP // vsldoi 6,0,6,12
+ VXOR IN1, TMP, IN1 // vxor 2,2,6
+
+ VXOR IN1, KEY, IN1 // vxor 2,2,3
+ JMP loop256 // b .Loop256
+
+done:
+ RET
+
+// func encryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路encryptBlockAsm(SB), NOSPLIT|NOFRAME, $0
+ MOVD nr+0(FP), R6 // Round count/Key size
+ MOVD xk+8(FP), R5 // Key pointer
+ MOVD dst+16(FP), R3 // Dest pointer
+ MOVD src+24(FP), R4 // Src pointer
+#ifdef GOARCH_ppc64le
+ MOVD $路rcon(SB), R7
+ LVX (R7), ESPERM // Permute value for P8_ macros.
+#endif
+
+ // Set CR{1,2,3}EQ to hold the key size information.
+ CMPU R6, $10, CR1
+ CMPU R6, $12, CR2
+ CMPU R6, $14, CR3
+
+ MOVD $16, R6
+ MOVD $32, R7
+ MOVD $48, R8
+ MOVD $64, R9
+ MOVD $80, R10
+ MOVD $96, R11
+ MOVD $112, R12
+
+ // Load text in BE order
+ P8_LXVB16X(R4, R0, V0)
+
+ // V1, V2 will hold keys, V0 is a temp.
+ // At completion, V2 will hold the ciphertext.
+ // Load xk[0:3] and xor with text
+ LXVD2X (R0+R5), V1
+ VXOR V0, V1, V0
+
+ // Load xk[4:11] and cipher
+ LXVD2X (R6+R5), V1
+ LXVD2X (R7+R5), V2
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Load xk[12:19] and cipher
+ LXVD2X (R8+R5), V1
+ LXVD2X (R9+R5), V2
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Load xk[20:27] and cipher
+ LXVD2X (R10+R5), V1
+ LXVD2X (R11+R5), V2
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Increment xk pointer to reuse constant offsets in R6-R12.
+ ADD $112, R5
+
+ // Load xk[28:35] and cipher
+ LXVD2X (R0+R5), V1
+ LXVD2X (R6+R5), V2
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Load xk[36:43] and cipher
+ LXVD2X (R7+R5), V1
+ LXVD2X (R8+R5), V2
+ BEQ CR1, Ldec_tail // Key size 10?
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Load xk[44:51] and cipher
+ LXVD2X (R9+R5), V1
+ LXVD2X (R10+R5), V2
+ BEQ CR2, Ldec_tail // Key size 12?
+ VCIPHER V0, V1, V0
+ VCIPHER V0, V2, V0
+
+ // Load xk[52:59] and cipher
+ LXVD2X (R11+R5), V1
+ LXVD2X (R12+R5), V2
+ BNE CR3, Linvalid_key_len // Not key size 14?
+ // Fallthrough to final cipher
+
+Ldec_tail:
+ // Cipher last two keys such that key information is
+ // cleared from V1 and V2.
+ VCIPHER V0, V1, V1
+ VCIPHERLAST V1, V2, V2
+
+ // Store the result in BE order.
+ P8_STXVB16X(V2, R3, R0)
+ RET
+
+Linvalid_key_len:
+ // Segfault, this should never happen. Only 3 keys sizes are created/used.
+ MOVD R0, 0(R0)
+ RET
+
+// func decryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+TEXT 路decryptBlockAsm(SB), NOSPLIT|NOFRAME, $0
+ MOVD nr+0(FP), R6 // Round count/Key size
+ MOVD xk+8(FP), R5 // Key pointer
+ MOVD dst+16(FP), R3 // Dest pointer
+ MOVD src+24(FP), R4 // Src pointer
+#ifdef GOARCH_ppc64le
+ MOVD $路rcon(SB), R7
+ LVX (R7), ESPERM // Permute value for P8_ macros.
+#endif
+
+ // Set CR{1,2,3}EQ to hold the key size information.
+ CMPU R6, $10, CR1
+ CMPU R6, $12, CR2
+ CMPU R6, $14, CR3
+
+ MOVD $16, R6
+ MOVD $32, R7
+ MOVD $48, R8
+ MOVD $64, R9
+ MOVD $80, R10
+ MOVD $96, R11
+ MOVD $112, R12
+
+ // Load text in BE order
+ P8_LXVB16X(R4, R0, V0)
+
+ // V1, V2 will hold keys, V0 is a temp.
+ // At completion, V2 will hold the text.
+ // Load xk[0:3] and xor with ciphertext
+ LXVD2X (R0+R5), V1
+ VXOR V0, V1, V0
+
+ // Load xk[4:11] and cipher
+ LXVD2X (R6+R5), V1
+ LXVD2X (R7+R5), V2
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Load xk[12:19] and cipher
+ LXVD2X (R8+R5), V1
+ LXVD2X (R9+R5), V2
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Load xk[20:27] and cipher
+ LXVD2X (R10+R5), V1
+ LXVD2X (R11+R5), V2
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Increment xk pointer to reuse constant offsets in R6-R12.
+ ADD $112, R5
+
+ // Load xk[28:35] and cipher
+ LXVD2X (R0+R5), V1
+ LXVD2X (R6+R5), V2
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Load xk[36:43] and cipher
+ LXVD2X (R7+R5), V1
+ LXVD2X (R8+R5), V2
+ BEQ CR1, Ldec_tail // Key size 10?
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Load xk[44:51] and cipher
+ LXVD2X (R9+R5), V1
+ LXVD2X (R10+R5), V2
+ BEQ CR2, Ldec_tail // Key size 12?
+ VNCIPHER V0, V1, V0
+ VNCIPHER V0, V2, V0
+
+ // Load xk[52:59] and cipher
+ LXVD2X (R11+R5), V1
+ LXVD2X (R12+R5), V2
+ BNE CR3, Linvalid_key_len // Not key size 14?
+ // Fallthrough to final cipher
+
+Ldec_tail:
+ // Cipher last two keys such that key information is
+ // cleared from V1 and V2.
+ VNCIPHER V0, V1, V1
+ VNCIPHERLAST V1, V2, V2
+
+ // Store the result in BE order.
+ P8_STXVB16X(V2, R3, R0)
+ RET
+
+Linvalid_key_len:
+ // Segfault, this should never happen. Only 3 keys sizes are created/used.
+ MOVD R0, 0(R0)
+ RET
+
+// Remove defines from above so they can be defined here
+#undef INP
+#undef OUTENC
+#undef ROUNDS
+#undef KEY
+#undef TMP
+
+// CBC encrypt or decrypt
+// R3 src
+// R4 dst
+// R5 len
+// R6 key
+// R7 iv
+// R8 enc=1 dec=0
+// Ported from: aes_p8_cbc_encrypt
+// Register usage:
+// R9: ROUNDS
+// R10: Index
+// V4: IV
+// V5: SRC
+// V7: DST
+
+#define INP R3
+#define OUT R4
+#define LEN R5
+#define KEY R6
+#define IVP R7
+#define ENC R8
+#define ROUNDS R9
+#define IDX R10
+
+#define RNDKEY0 V0
+#define INOUT V2
+#define TMP V3
+
+#define IVEC V4
+
+// Vector loads are done using LVX followed by
+// a VPERM using mask generated from previous
+// LVSL or LVSR instruction, to obtain the correct
+// bytes if address is unaligned.
+
+// Encryption is done with VCIPHER and VCIPHERLAST
+// Decryption is done with VNCIPHER and VNCIPHERLAST
+
+// Encrypt and decypt is done as follows:
+// - INOUT value is initialized in outer loop.
+// - ROUNDS value is adjusted for loop unrolling.
+// - Encryption/decryption is done in loop based on
+// adjusted ROUNDS value.
+// - Final INOUT value is encrypted/decrypted and stored.
+
+// Note: original implementation had an 8X version
+// for decryption which was omitted to avoid the
+// complexity.
+
+// func cryptBlocksChain(src, dst *byte, length int, key *uint32, iv *byte, enc int, nr int)
+TEXT 路cryptBlocksChain(SB), NOSPLIT|NOFRAME, $0
+ MOVD src+0(FP), INP
+ MOVD dst+8(FP), OUT
+ MOVD length+16(FP), LEN
+ MOVD key+24(FP), KEY
+ MOVD iv+32(FP), IVP
+ MOVD enc+40(FP), ENC
+ MOVD nr+48(FP), ROUNDS
+
+#ifdef GOARCH_ppc64le
+ MOVD $路rcon(SB), R11
+ LVX (R11), ESPERM // Permute value for P8_ macros.
+#endif
+
+ CMPU LEN, $16 // cmpldi r5,16
+ BC 14, 0, LR // bltlr-, return if len < 16.
+ CMPW ENC, $0 // cmpwi r8,0
+
+ P8_LXVB16X(IVP, R0, IVEC) // load ivec in BE register order
+
+ SRW $1, ROUNDS // rlwinm r9,r9,31,1,31
+ MOVD $0, IDX // li r10,0
+ ADD $-1, ROUNDS // addi r9,r9,-1
+ BEQ Lcbc_dec // beq
+ PCALIGN $16
+
+ // Outer loop: initialize encrypted value (INOUT)
+ // Load input (INPTAIL) ivec (IVEC)
+Lcbc_enc:
+ P8_LXVB16X(INP, R0, INOUT) // load text in BE vreg order
+ ADD $16, INP // addi r3,r3,16
+ MOVD ROUNDS, CTR // mtctr r9
+ ADD $-16, LEN // addi r5,r5,-16
+ LXVD2X (KEY+IDX), RNDKEY0 // load first xkey
+ ADD $16, IDX // addi r10,r10,16
+ VXOR INOUT, RNDKEY0, INOUT // vxor v2,v2,v0
+ VXOR INOUT, IVEC, INOUT // vxor v2,v2,v4
+
+ // Encryption loop of INOUT using RNDKEY0
+Loop_cbc_enc:
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ VCIPHER INOUT, RNDKEY0, INOUT // vcipher v2,v2,v1
+ ADD $16, IDX // addi r10,r10,16
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ VCIPHER INOUT, RNDKEY0, INOUT // vcipher v2,v2,v1
+ ADD $16, IDX // addi r10,r10,16
+ BDNZ Loop_cbc_enc
+
+ // Encrypt tail values and store INOUT
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ VCIPHER INOUT, RNDKEY0, INOUT // vcipher v2,v2,v1
+ ADD $16, IDX // addi r10,r10,16
+ LXVD2X (KEY+IDX), RNDKEY0 // load final xkey
+ VCIPHERLAST INOUT, RNDKEY0, IVEC // vcipherlast v4,v2,v0
+ MOVD $0, IDX // reset key index for next block
+ CMPU LEN, $16 // cmpldi r5,16
+ P8_STXVB16X(IVEC, OUT, R0) // store ciphertext in BE order
+ ADD $16, OUT // addi r4,r4,16
+ BGE Lcbc_enc // bge Lcbc_enc
+ BR Lcbc_done // b Lcbc_done
+
+ // Outer loop: initialize decrypted value (INOUT)
+ // Load input (INPTAIL) ivec (IVEC)
+Lcbc_dec:
+ P8_LXVB16X(INP, R0, TMP) // load ciphertext in BE vreg order
+ ADD $16, INP // addi r3,r3,16
+ MOVD ROUNDS, CTR // mtctr r9
+ ADD $-16, LEN // addi r5,r5,-16
+ LXVD2X (KEY+IDX), RNDKEY0 // load first xkey
+ ADD $16, IDX // addi r10,r10,16
+ VXOR TMP, RNDKEY0, INOUT // vxor v2,v3,v0
+ PCALIGN $16
+
+ // Decryption loop of INOUT using RNDKEY0
+Loop_cbc_dec:
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ ADD $16, IDX // addi r10,r10,16
+ VNCIPHER INOUT, RNDKEY0, INOUT // vncipher v2,v2,v1
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ ADD $16, IDX // addi r10,r10,16
+ VNCIPHER INOUT, RNDKEY0, INOUT // vncipher v2,v2,v0
+ BDNZ Loop_cbc_dec
+
+ // Decrypt tail values and store INOUT
+ LXVD2X (KEY+IDX), RNDKEY0 // load next xkey
+ ADD $16, IDX // addi r10,r10,16
+ VNCIPHER INOUT, RNDKEY0, INOUT // vncipher v2,v2,v1
+ LXVD2X (KEY+IDX), RNDKEY0 // load final xkey
+ MOVD $0, IDX // li r10,0
+ VNCIPHERLAST INOUT, RNDKEY0, INOUT // vncipherlast v2,v2,v0
+ CMPU LEN, $16 // cmpldi r5,16
+ VXOR INOUT, IVEC, INOUT // vxor v2,v2,v4
+ VOR TMP, TMP, IVEC // vor v4,v3,v3
+ P8_STXVB16X(INOUT, OUT, R0) // store text in BE order
+ ADD $16, OUT // addi r4,r4,16
+ BGE Lcbc_dec // bge
+
+Lcbc_done:
+ VXOR RNDKEY0, RNDKEY0, RNDKEY0 // clear key register
+ P8_STXVB16X(IVEC, R0, IVP) // Save ivec in BE order for next round.
+ RET // bclr 20,lt,0
+
diff --git a/pkg/slayers/path/hummingbird/base.go b/pkg/slayers/path/hummingbird/base.go
new file mode 100644
index 0000000000..fbfd63d83d
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/base.go
@@ -0,0 +1,171 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird
+
+import (
+ "encoding/binary"
+ "fmt"
+
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/slayers/path"
+)
+
+const MetaLen = 12
+
+func RegisterPath() {
+ path.RegisterPath(path.Metadata{
+ Type: PathType,
+ Desc: "Hummingbird",
+ New: func() path.Path {
+ return &Raw{}
+ },
+ })
+}
+
+// Base holds the basic information that is used by both raw and fully decoded paths.
+type Base struct {
+ // PathMeta is the Hummingbird path meta header. It is always instantiated when
+ // decoding a path from bytes.
+ PathMeta MetaHdr
+ // NumINF is the number of InfoFields in the path.
+ NumINF int
+ // NumLines is the number of 4 bytes lines in the path. NumLines = SegLen[i] for 0<=i<=2.
+ NumLines int
+}
+
+// DecodeFromBytes populates the fields from a raw buffer. The buffer must be of length >=
+// hummingbird.MetaLen.
+func (s *Base) DecodeFromBytes(data []byte) error {
+ // PathMeta checks bounds.
+ err := s.PathMeta.DecodeFromBytes(data)
+ if err != nil {
+ return err
+ }
+ s.NumINF = 0
+ s.NumLines = 0
+ for i := 2; i >= 0; i-- {
+ if s.PathMeta.SegLen[i] == 0 && s.NumINF > 0 {
+ return serrors.New(
+ fmt.Sprintf("Meta.SegLen[%d] == 0, but Meta.SegLen[%d] > 0", i, s.NumINF-1))
+ }
+ if s.PathMeta.SegLen[i] > 0 && s.NumINF == 0 {
+ s.NumINF = i + 1
+ }
+ s.NumLines += int(s.PathMeta.SegLen[i])
+ }
+ return nil
+}
+
+// IncPath increases the currHF index by n and the currINF index if appropriate.
+func (s *Base) IncPath(n int) error {
+ if s.NumINF == 0 {
+ return serrors.New("empty path cannot be increased")
+ }
+ if int(s.PathMeta.CurrHF) >= s.NumLines-n {
+ return serrors.New("Incrementing path over end")
+ }
+ s.PathMeta.CurrHF += uint8(n)
+ s.PathMeta.CurrINF = s.InfIndexForHF(s.PathMeta.CurrHF)
+ return nil
+}
+
+// IsXover returns whether we are at a crossover point.
+func (s *Base) IsXover() bool {
+ return s.PathMeta.CurrHF+FlyoverLines < uint8(s.NumLines) &&
+ (s.PathMeta.CurrINF != s.InfIndexForHF(s.PathMeta.CurrHF+HopLines) ||
+ s.PathMeta.CurrINF != s.InfIndexForHF(s.PathMeta.CurrHF+FlyoverLines))
+
+}
+
+// IsFirstHopAfterXover returns whether this is the first hop field after a crossover point.
+func (s *Base) IsFirstHopAfterXover() bool {
+ return s.PathMeta.CurrINF > 0 && s.PathMeta.CurrHF > 0 &&
+ s.PathMeta.CurrINF-1 == s.InfIndexForHF(s.PathMeta.CurrHF-1)
+}
+
+// InfIndexForHF returns the segment to which the HopField hf belongs
+// The argument hfLines is the line count until the first line of this hop field.
+func (s *Base) InfIndexForHF(hfLines uint8) uint8 {
+ switch {
+ case hfLines < s.PathMeta.SegLen[0]:
+ return 0
+ case hfLines < s.PathMeta.SegLen[0]+s.PathMeta.SegLen[1]:
+ return 1
+ default:
+ return 2
+ }
+}
+
+// Len returns the length of the path in bytes.
+func (s *Base) Len() int {
+ return MetaLen + s.NumINF*path.InfoLen + s.NumLines*LineLen
+}
+
+// Type returns the type of the path.
+func (s *Base) Type() path.Type {
+ return PathType
+}
+
+// MetaHdr is the PathMetaHdr of a Hummingbird (data-plane) path type.
+type MetaHdr struct {
+ CurrINF uint8 // Index of the current info field.
+ CurrHF uint8 // Index of the current hop field.
+ SegLen [3]uint8 // Length in bytes / 4 of each segment.
+ BaseTS uint32
+ HighResTS uint32
+}
+
+// DecodeFromBytes populates the fields from a raw buffer. The buffer must be of length >=
+// hummingbird.MetaLen.
+func (m *MetaHdr) DecodeFromBytes(raw []byte) error {
+ if len(raw) < MetaLen {
+ return serrors.New("MetaHdr raw too short", "expected", MetaLen, "actual", len(raw))
+ }
+ line := binary.BigEndian.Uint32(raw[0:4])
+ m.CurrINF = uint8(line >> 30)
+ m.CurrHF = uint8(line >> 22)
+ m.SegLen[0] = uint8(line>>14) & 0x7F
+ m.SegLen[1] = uint8(line>>7) & 0x7F
+ m.SegLen[2] = uint8(line) & 0x7F
+
+ m.BaseTS = binary.BigEndian.Uint32(raw[4:8])
+ m.HighResTS = binary.BigEndian.Uint32(raw[8:12])
+
+ return nil
+}
+
+// SerializeTo writes the fields into the provided buffer. The buffer must be of length >=
+// hummingbird.MetaLen.
+func (m *MetaHdr) SerializeTo(b []byte) error {
+ if len(b) < MetaLen {
+ return serrors.New("buffer for MetaHdr too short", "expected", MetaLen, "actual", len(b))
+ }
+ line := uint32(m.CurrINF)<<30 | uint32(m.CurrHF)<<22
+ line |= uint32(m.SegLen[0]&0x7F) << 14
+ line |= uint32(m.SegLen[1]&0x7F) << 7
+ line |= uint32(m.SegLen[2] & 0x7F)
+ binary.BigEndian.PutUint32(b[0:4], line)
+
+ binary.BigEndian.PutUint32(b[4:8], m.BaseTS)
+ binary.BigEndian.PutUint32(b[8:12], m.HighResTS)
+
+ return nil
+}
+
+func (m MetaHdr) String() string {
+ return fmt.Sprintf(
+ "{CurrInf: %d, CurrHF: %d, SegLen: %v, BaseTimestamp: %v, HighResTimestamp: %v}",
+ m.CurrINF, m.CurrHF, m.SegLen, m.BaseTS, m.HighResTS)
+}
diff --git a/pkg/slayers/path/hummingbird/base_test.go b/pkg/slayers/path/hummingbird/base_test.go
new file mode 100644
index 0000000000..cd1fb8f908
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/base_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+func TestIncPath(t *testing.T) {
+ testCases := map[string]struct {
+ nsegs, nhops int
+ segLens [3]uint8
+ inIdxs, wantIdxs [][2]int
+ }{
+ "1 segment, 2 hops": {
+ nsegs: 1,
+ nhops: 6,
+ segLens: [3]uint8{6, 0, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}},
+ wantIdxs: [][2]int{{0, 3}, {0, 0}},
+ },
+ "1 segment, 5 hops": {
+ nsegs: 1,
+ nhops: 15,
+ segLens: [3]uint8{15, 0, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}, {0, 6}, {0, 9}, {0, 12}},
+ wantIdxs: [][2]int{{0, 3}, {0, 6}, {0, 9}, {0, 12}, {0, 0}},
+ },
+ "2 segments, 5 hops": {
+ nsegs: 2,
+ nhops: 15,
+ segLens: [3]uint8{6, 9, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}, {1, 6}, {1, 9}, {1, 12}},
+ wantIdxs: [][2]int{{0, 3}, {1, 6}, {1, 9}, {1, 12}, {0, 0}},
+ },
+ "3 segments, 9 hops": {
+ nsegs: 3,
+ nhops: 27,
+ segLens: [3]uint8{6, 12, 9},
+ inIdxs: [][2]int{
+ {0, 0}, {0, 3}, {1, 6}, {1, 9}, {1, 12}, {1, 15}, {2, 18}, {2, 21}, {2, 24},
+ },
+ wantIdxs: [][2]int{
+ {0, 3}, {1, 6}, {1, 9}, {1, 12}, {1, 15}, {2, 18}, {2, 21}, {2, 24}, {0, 0},
+ },
+ },
+ }
+
+ for name, tc := range testCases {
+ name, tc := name, tc
+ for i := range tc.inIdxs {
+ i := i
+ t.Run(fmt.Sprintf("%s case %d", name, i+1), func(t *testing.T) {
+ t.Parallel()
+ s := hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: uint8(tc.inIdxs[i][0]),
+ CurrHF: uint8(tc.inIdxs[i][1]),
+ SegLen: tc.segLens,
+ },
+ NumINF: tc.nsegs,
+ NumLines: tc.nhops,
+ }
+ err := s.IncPath(3)
+ if tc.wantIdxs[i][0] == 0 && tc.wantIdxs[i][1] == 0 {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, uint8(tc.wantIdxs[i][0]), s.PathMeta.CurrINF, "CurrINF")
+ assert.Equal(t, uint8(tc.wantIdxs[i][1]), s.PathMeta.CurrHF, "CurrHF")
+ }
+
+ })
+ }
+ }
+}
+
+func TestBaseIsXOver(t *testing.T) {
+ testCases := map[string]struct {
+ nsegs, nhops int
+ segLens [3]uint8
+ inIdxs [][2]int
+ xover []bool
+ }{
+ "1 segment, 2 hops": {
+ nsegs: 1,
+ nhops: 6,
+ segLens: [3]uint8{6, 0, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}},
+ xover: []bool{false, false},
+ },
+ "1 segment, 5 hops": {
+ nsegs: 1,
+ nhops: 19,
+ segLens: [3]uint8{19, 0, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}, {0, 8}, {0, 13}, {0, 16}},
+ xover: []bool{false, false, false, false, false},
+ },
+ "2 segments, 5 hops": {
+ nsegs: 2,
+ nhops: 17,
+ segLens: [3]uint8{8, 9, 0},
+ inIdxs: [][2]int{{0, 0}, {0, 3}, {1, 8}, {1, 11}, {1, 14}},
+ xover: []bool{false, true, false, false, false},
+ },
+ "3 segments, 9 hops": {
+ nsegs: 3,
+ nhops: 37,
+ segLens: [3]uint8{6, 16, 15},
+ inIdxs: [][2]int{
+ {0, 0}, {0, 3}, {1, 6}, {1, 11}, {1, 14}, {1, 19}, {2, 22}, {2, 27}, {2, 32},
+ },
+ xover: []bool{false, true, false, false, false, true, false, false, false},
+ },
+ }
+
+ for name, tc := range testCases {
+ name, tc := name, tc
+ for i := range tc.xover {
+ i := i
+ s := hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: uint8(tc.inIdxs[i][0]),
+ CurrHF: uint8(tc.inIdxs[i][1]),
+ SegLen: tc.segLens,
+ },
+ NumINF: tc.nsegs,
+ NumLines: tc.nhops,
+ }
+ t.Run(fmt.Sprintf("%s case %d", name, i+1), func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, tc.xover[i], s.IsXover())
+ })
+ t.Run(fmt.Sprintf("%s case %d IsFirstAfterXover", name, i+1), func(t *testing.T) {
+ t.Parallel()
+ firstHopAfterXover := false
+ if i > 0 {
+ firstHopAfterXover = tc.xover[i-1]
+ }
+ assert.Equal(t, firstHopAfterXover, s.IsFirstHopAfterXover())
+ })
+ }
+ }
+}
diff --git a/pkg/slayers/path/hummingbird/decoded.go b/pkg/slayers/path/hummingbird/decoded.go
new file mode 100644
index 0000000000..dc2d3efae8
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/decoded.go
@@ -0,0 +1,259 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird
+
+import (
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/scion"
+)
+
+const (
+ // MaxINFs is the maximum number of info fields in a Hummingbird path.
+ MaxINFs = 3
+ // MaxHops is the maximum number of hop fields in a Hummingbird path.
+ MaxHops = 85
+)
+
+// Decoded implements the Hummingbird (data-plane) path type. Decoded is intended to be used in
+// non-performance critical code paths, where the convenience of having a fully parsed path trumps
+// the loss of performance.
+type Decoded struct {
+ Base
+ // InfoFields contains all the InfoFields of the path.
+ InfoFields []path.InfoField
+ // HopFields contains all the HopFields of the path.
+ HopFields []FlyoverHopField
+ // FirstHopPerSeg notes the index of the first hopfield of the second and third segment
+ FirstHopPerSeg [2]uint8
+}
+
+// DecodeFromBytes fully decodes the Hummingbird path into the corresponding fields.
+func (s *Decoded) DecodeFromBytes(data []byte) error {
+ if err := s.Base.DecodeFromBytes(data); err != nil {
+ return err
+ }
+ if minLen := s.Len(); len(data) < minLen {
+ return serrors.New("DecodedPath raw too short", "expected", minLen, "actual", len(data))
+ }
+
+ offset := MetaLen
+ s.InfoFields = make([]path.InfoField, s.NumINF)
+ for i := 0; i < s.NumINF; i++ {
+ if err := s.InfoFields[i].DecodeFromBytes(data[offset : offset+path.InfoLen]); err != nil {
+ return err
+ }
+ offset += path.InfoLen
+ }
+
+ // Allocate maximum number of possible hopfields based on length
+ s.HopFields = make([]FlyoverHopField, s.NumLines/HopLines)
+ i, j := 0, 0
+ // If last hop is not a flyover hop, decode it with only 12 bytes slice
+ for ; j < s.NumLines-HopLines; i++ {
+ if err := s.HopFields[i].DecodeFromBytes(data[offset : offset+flyoverLen]); err != nil {
+ return err
+ }
+ // Set FirstHopPerSeg
+ if j == int(s.PathMeta.SegLen[0]) {
+ s.FirstHopPerSeg[0] = uint8(i)
+ } else if j == int(s.PathMeta.SegLen[0])+int(s.PathMeta.SegLen[1]) {
+ s.FirstHopPerSeg[1] = uint8(i)
+ }
+
+ if s.HopFields[i].Flyover {
+ offset += flyoverLen
+ j += FlyoverLines
+ } else {
+ offset += hopLen
+ j += HopLines
+ }
+ }
+ if j == s.NumLines-HopLines {
+ if err := s.HopFields[i].DecodeFromBytes(data[offset : offset+hopLen]); err != nil {
+ return err
+ }
+ i++
+ }
+ s.HopFields = s.HopFields[:i]
+ if s.PathMeta.SegLen[1] == 0 {
+ s.FirstHopPerSeg[0] = uint8(i)
+ s.FirstHopPerSeg[1] = uint8(i)
+ } else if s.PathMeta.SegLen[2] == 0 {
+ s.FirstHopPerSeg[1] = uint8(i)
+ }
+
+ return nil
+}
+
+// SerializeTo writes the path to a slice. The slice must be big enough to hold the entire data,
+// otherwise an error is returned.
+func (s *Decoded) SerializeTo(b []byte) error {
+ if len(b) < s.Len() {
+ return serrors.New("buffer too small to serialize path.", "expected", s.Len(),
+ "actual", len(b))
+ }
+ var offset int
+
+ offset = MetaLen
+ if err := s.PathMeta.SerializeTo(b[:MetaLen]); err != nil {
+ return err
+ }
+
+ for _, info := range s.InfoFields {
+ if err := info.SerializeTo(b[offset : offset+path.InfoLen]); err != nil {
+ return err
+ }
+ offset += path.InfoLen
+ }
+ for _, hop := range s.HopFields {
+ if hop.Flyover {
+ if err := hop.SerializeTo(b[offset : offset+flyoverLen]); err != nil {
+ return err
+ }
+ offset += flyoverLen
+ } else {
+ if err := hop.SerializeTo(b[offset : offset+hopLen]); err != nil {
+ return err
+ }
+ offset += hopLen
+ }
+ }
+ return nil
+}
+
+// Reverse reverses a hummingbird path.
+// Removes all reservations from a Hummingbird path, as these are not bidirectional
+func (s *Decoded) Reverse() (path.Path, error) {
+ if s.NumINF == 0 {
+ return nil, serrors.New("empty decoded path is invalid and cannot be reversed")
+ }
+
+ if err := s.removeFlyovers(); err != nil {
+ return nil, err
+ }
+ // Reverse order of InfoFields and SegLens
+ for i, j := 0, s.NumINF-1; i < j; i, j = i+1, j-1 {
+ s.InfoFields[i], s.InfoFields[j] = s.InfoFields[j], s.InfoFields[i]
+ s.PathMeta.SegLen[i], s.PathMeta.SegLen[j] = s.PathMeta.SegLen[j], s.PathMeta.SegLen[i]
+ }
+ // Reverse cons dir flags
+ for i := 0; i < s.NumINF; i++ {
+ info := &s.InfoFields[i]
+ info.ConsDir = !info.ConsDir
+ }
+ // Reverse order of hop fields
+ for i, j := 0, len(s.HopFields)-1; i < j; i, j = i+1, j-1 {
+ s.HopFields[i], s.HopFields[j] = s.HopFields[j], s.HopFields[i]
+ }
+ // Update CurrINF and CurrHF and SegLens
+ s.PathMeta.CurrINF = uint8(s.NumINF) - s.PathMeta.CurrINF - 1
+ s.PathMeta.CurrHF = uint8(s.NumLines) - s.PathMeta.CurrHF - HopLines
+
+ return s, nil
+}
+
+// RemoveFlyovers removes all reservations from a decoded path
+// Corrects SegLen and CurrHF accordingly
+// Does not affect MACs
+func (s *Decoded) removeFlyovers() error {
+ var idxInf uint8 = 0
+ var offset uint8 = 0
+ var segCount uint8 = 0
+
+ for i, hop := range s.HopFields {
+ if idxInf > 2 {
+ return serrors.New("path appears to have more than 3 segments during flyover removal")
+ }
+ if hop.Flyover {
+ s.HopFields[i].Flyover = false
+
+ if s.PathMeta.CurrHF > offset {
+ s.PathMeta.CurrHF -= 2
+ }
+ s.Base.NumLines -= 2
+ s.PathMeta.SegLen[idxInf] -= 2
+ }
+ segCount += HopLines
+ if s.PathMeta.SegLen[idxInf] == segCount {
+ segCount = 0
+ idxInf += 1
+ } else if s.PathMeta.SegLen[idxInf] < segCount {
+ return serrors.New(
+ "New hopfields boundaries do not match new segment lengths after flyover removal")
+ }
+ offset += HopLines
+ }
+ return nil
+}
+
+// ToRaw tranforms hummingbird.Decoded into hummingbird.Raw.
+func (s *Decoded) ToRaw() (*Raw, error) {
+ b := make([]byte, s.Len())
+ if err := s.SerializeTo(b); err != nil {
+ return nil, err
+ }
+ raw := &Raw{}
+ if err := raw.DecodeFromBytes(b); err != nil {
+ return nil, err
+ }
+ return raw, nil
+}
+
+// InfIndexForHFIndex takes the index of the hop field in the HopFields slice and returns its
+// corresponding info field index in the InfoFields slice. Expected 0 <= hfIdx < len(HopFields).
+func (s *Decoded) InfIndexForHFIndex(hfIdx uint8) uint8 {
+ lineCount := uint8(0)
+ for i := uint8(0); i < hfIdx; i++ {
+ if s.HopFields[i].Flyover {
+ lineCount += FlyoverLines
+ } else {
+ lineCount += HopLines
+ }
+ }
+ return s.InfIndexForHF(lineCount)
+}
+
+// Converts a SCiON decoded path to a hummingbird decoded path
+// Does NOT perform a deep copy of hop and info fields.
+// Does NOT set the PathMeta Timestamps and counter
+func (s *Decoded) ConvertFromScionDecoded(d scion.Decoded) {
+ // convert Base
+ s.convertBaseFromScion(d.Base)
+ // transfer Infofields
+ s.InfoFields = d.InfoFields
+ // convert HopFields
+ s.HopFields = make([]FlyoverHopField, d.NumHops)
+ for i, hop := range d.HopFields {
+ s.HopFields[i] = FlyoverHopField{
+ HopField: hop,
+ Flyover: false,
+ }
+ }
+ s.FirstHopPerSeg[0] = d.Base.PathMeta.SegLen[0]
+ s.FirstHopPerSeg[1] = d.Base.PathMeta.SegLen[0] + d.Base.PathMeta.SegLen[1]
+}
+
+func (s *Decoded) convertBaseFromScion(d scion.Base) {
+ s.Base.NumINF = d.NumINF
+ s.Base.PathMeta.CurrINF = d.PathMeta.CurrINF
+
+ s.Base.NumLines = d.NumHops * HopLines
+ s.Base.PathMeta.CurrHF = d.PathMeta.CurrHF * HopLines
+
+ s.Base.PathMeta.SegLen[0] = d.PathMeta.SegLen[0] * HopLines
+ s.Base.PathMeta.SegLen[1] = d.PathMeta.SegLen[1] * HopLines
+ s.Base.PathMeta.SegLen[2] = d.PathMeta.SegLen[2] * HopLines
+}
diff --git a/pkg/slayers/path/hummingbird/decoded_test.go b/pkg/slayers/path/hummingbird/decoded_test.go
new file mode 100644
index 0000000000..dab9107b2d
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/decoded_test.go
@@ -0,0 +1,248 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+ "github.com/scionproto/scion/pkg/slayers/path/scion"
+)
+
+func TestDecodedSerializeHbird(t *testing.T) {
+ for i := range decodedPaths {
+ b := make([]byte, decodedPaths[i].Len())
+ assert.NoError(t, decodedPaths[i].SerializeTo(b))
+ assert.Equal(t, decodedBytes[i], b)
+ }
+}
+
+func TestDecodeFromBytesHbird(t *testing.T) {
+ s := &hummingbird.Decoded{}
+ for i := range decodedPaths {
+ assert.NoError(t, s.DecodeFromBytes(decodedBytes[i]))
+ assert.Equal(t, decodedPaths[i], s)
+ }
+}
+
+func TestSerializeAndBack(t *testing.T) {
+ for i := range decodedPaths {
+ buff := make([]byte, decodedPaths[i].Len())
+ assert.NoError(t, decodedPaths[i].SerializeTo(buff))
+ s := &hummingbird.Decoded{}
+ assert.NoError(t, s.DecodeFromBytes(buff))
+ assert.Equal(t, decodedPaths[i], s)
+ }
+}
+
+func TestDecodedDecodeFromBytesNoFlyovers(t *testing.T) {
+ // p is the scion decoded path we would observe using the Tiny topology of the
+ // topology generator, when going from 111 to 112. This is one up segment with 2 hops, followed
+ // by a down segment with two hops as well. There is a cross over at core 110 gluing both.
+ p := &scion.Decoded{
+ Base: scion.Base{
+ PathMeta: scion.MetaHdr{
+ SegLen: [3]uint8{2, 2, 0},
+ },
+ NumINF: 2,
+ NumHops: 4,
+ },
+ InfoFields: []path.InfoField{
+ {}, // up
+ {}, // down
+ },
+ HopFields: []path.HopField{
+ {}, // 111: 0->41 up
+ {}, // 110: 1->0 up
+ {}, // 110: 0->2 down
+ {}, // 112: 1->0 down
+ },
+ }
+
+ // Create a hummingbird path from the scion one.
+ hbird := &hummingbird.Decoded{}
+ hbird.ConvertFromScionDecoded(*p) // SegLen will be [6,6,0] after this
+
+ // Check the hummingbird path is correct by serializing and deserializing it.
+ buf := make([]byte, hbird.Len())
+ err := hbird.SerializeTo(buf)
+ assert.NoError(t, err)
+ // Deserialize.
+ hbird = &hummingbird.Decoded{}
+ err = hbird.DecodeFromBytes(buf)
+ assert.NoError(t, err)
+}
+
+func TestDecodedReverseHbird(t *testing.T) {
+ for name, tc := range pathReverseTestCases {
+ name, tc := name, tc
+ for i := range tc.inIdxs {
+ i := i
+ t.Run(fmt.Sprintf("%s case %d", name, i+1), func(t *testing.T) {
+ t.Parallel()
+ inputPath := mkDecodedHbirdPath(t, tc.input, uint8(tc.inIdxs[i][0]),
+ uint8(tc.inIdxs[i][1]))
+ wantPath := mkDecodedHbirdPath(t, tc.want, uint8(tc.wantIdxs[i][0]),
+ uint8(tc.wantIdxs[i][1]))
+ revPath, err := inputPath.Reverse()
+ assert.NoError(t, err)
+ assert.Equal(t, wantPath, revPath)
+ })
+ }
+ }
+}
+
+func TestEmptyDecodedReverse(t *testing.T) {
+ emptyDecodedTestPath := &hummingbird.Decoded{
+ Base: hummingbird.Base{},
+ InfoFields: []path.InfoField{},
+ HopFields: []hummingbird.FlyoverHopField{},
+ }
+ _, err := emptyDecodedTestPath.Reverse()
+ assert.Error(t, err)
+}
+
+func TestDecodedToRaw(t *testing.T) {
+ raw, err := decodedPaths[0].ToRaw()
+ assert.NoError(t, err)
+ assert.Equal(t, rawHbirdTestPath, raw)
+}
+
+func TestInfIndexForHFIndex(t *testing.T) {
+ cases := map[string]struct {
+ path hummingbird.Decoded
+ expected []uint8 // the INF indices of each hop field in the test case
+ }{
+ "empty": {
+ path: hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ SegLen: [3]uint8{0, 0, 0},
+ },
+ },
+ },
+ },
+ "one_segment_o": {
+ path: hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ SegLen: [3]uint8{3, 0, 0},
+ },
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {Flyover: false},
+ },
+ },
+ expected: []uint8{0},
+ },
+ // one_segment_oxx means there is one segment with three hops, first is not flyover,
+ // second and third are.
+ "one_segment_oxx": {
+ path: hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ SegLen: [3]uint8{13, 0, 0},
+ },
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {Flyover: false},
+ {Flyover: true},
+ {Flyover: true},
+ },
+ },
+ expected: []uint8{0, 0, 0},
+ },
+ "two_segments_o_oxx": {
+ path: hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ SegLen: [3]uint8{3, 13, 0},
+ },
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {Flyover: false},
+ {Flyover: false},
+ {Flyover: true},
+ {Flyover: true},
+ },
+ },
+ expected: []uint8{0, 1, 1, 1},
+ },
+ }
+ for name, tc := range cases {
+ name, tc := name, tc
+ t.Run(name, func(t *testing.T) {
+ for i := range tc.path.HopFields {
+ got := tc.path.InfIndexForHFIndex(uint8(i))
+ assert.Equal(t, tc.expected[i], got)
+ }
+ assert.Panics(t, func() {
+ tc.path.InfIndexForHFIndex(uint8(len(tc.path.HopFields)) + 1)
+ })
+ })
+ }
+}
+
+func mkDecodedHbirdPath(
+ t *testing.T,
+ pcase hbirdPathCase,
+ infIdx uint8,
+ hopIdx uint8,
+) *hummingbird.Decoded {
+ t.Helper()
+ s := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: infIdx,
+ CurrHF: hopIdx,
+ BaseTS: 14,
+ HighResTS: 15,
+ },
+ },
+ }
+ for _, dir := range pcase.infos {
+ s.InfoFields = append(s.InfoFields, path.InfoField{ConsDir: dir})
+ }
+ i := 0
+ for j, hops := range pcase.hops {
+ for _, hop := range hops {
+ isFlyover := hop[1] == 1
+ s.HopFields = append(s.HopFields,
+ hummingbird.FlyoverHopField{
+ HopField: path.HopField{
+ ConsIngress: hop[0],
+ ConsEgress: hop[0],
+ Mac: [6]byte{1, 2, 3, 4, 5, 6}},
+ Flyover: isFlyover,
+ Duration: 2,
+ })
+ if isFlyover {
+ i += 5
+ s.PathMeta.SegLen[j] += 5
+ } else {
+ i += 3
+ s.PathMeta.SegLen[j] += 3
+ }
+ }
+ }
+ s.NumINF = len(pcase.infos)
+ s.NumLines = i
+
+ return s
+}
diff --git a/pkg/slayers/path/hummingbird/export_test.go b/pkg/slayers/path/hummingbird/export_test.go
new file mode 100644
index 0000000000..10a436fa5f
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/export_test.go
@@ -0,0 +1,17 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird
+
+var ExportedFlyoverLen = flyoverLen
diff --git a/pkg/slayers/path/hummingbird/flyoverhopfield.go b/pkg/slayers/path/hummingbird/flyoverhopfield.go
new file mode 100644
index 0000000000..b019300fc6
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/flyoverhopfield.go
@@ -0,0 +1,109 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird
+
+import (
+ "encoding/binary"
+
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/slayers/path"
+)
+
+const (
+ // macOffset is the offset of the MAC field from the beginning of the HopField.
+ macOffset = 6
+
+ // LineLen is the number of bytes in a line as considered by CurrHF in the PathMetaHeader.
+ LineLen = 4
+
+ // The number of lines in a hopfield.
+ HopLines = 3
+
+ // The number of lines in a flyoverhopfield.
+ FlyoverLines = 5
+
+ // hopLen is the size of a HopField in bytes.
+ hopLen = LineLen * HopLines
+
+ // HopLen is the size of a FlyoverHopField in bytes.
+ flyoverLen = LineLen * FlyoverLines
+)
+
+type FlyoverHopField struct {
+ // SCiON Hopfield part of the FlyoverHopField.
+ HopField path.HopField
+ // True if flyover is present.
+ Flyover bool
+ // ResID is the Reservation ID of the flyover.
+ ResID uint32
+ // Bw is the reserved banwidth of the flyover.
+ Bw uint16
+ // ResStartTime is the start time of the reservation,
+ // as a negative offset from the BaseTimeStamp in the PathMetaHdr.
+ ResStartTime uint16
+ // Duration is the duration of the reservation.
+ Duration uint16
+}
+
+// DecodeFromBytes populates the fields from a raw buffer.
+// The buffer must be of length >= HopLen if the Flyover bit is false
+// The buffer must be of length >= FlyoverLen if the Flyover bit is set
+// DecodeFromBytes modifies the fields of *h and reads (but does not modify) the contents of raw.
+// When a call that satisfies the precondition (len(raw) >= HopLen) is made,
+// the return value is guaranteed to be nil.
+// Calls to DecodeFromBytes are always guaranteed to terminate.
+func (h *FlyoverHopField) DecodeFromBytes(raw []byte) (err error) {
+ if err := h.HopField.DecodeFromBytes(raw); err != nil {
+ return err
+ }
+ h.Flyover = raw[0]&0x80 == 0x80
+ if h.Flyover {
+ if len(raw) < flyoverLen {
+ return serrors.New("FlyoverHopField raw too short", "expected",
+ flyoverLen, "actual", len(raw))
+ }
+ h.ResID = binary.BigEndian.Uint32(raw[12:16]) >> 10
+ h.Bw = binary.BigEndian.Uint16(raw[14:16]) & 0x03ff
+ h.ResStartTime = binary.BigEndian.Uint16(raw[16:18])
+ h.Duration = binary.BigEndian.Uint16(raw[18:20])
+ }
+ return nil
+}
+
+// SerializeTo writes the fields into the provided buffer.
+// The buffer must be of length >= HopLen if the Flyover bit is false
+// The buffer must be of length >= FlyoverLen if the Flyover bit is set
+// SerializeTo reads (but does not modify) the fields of *h and writes to the contents of b.
+// When a call that satisfies the precondition (len(b) >= HopLen) is made,
+// the return value is guaranteed to be nil.
+// Calls to SerializeTo are guaranteed to terminate.
+func (h *FlyoverHopField) SerializeTo(b []byte) (err error) {
+ if err := h.HopField.SerializeTo(b); err != nil {
+ return err
+ }
+
+ if h.Flyover {
+ if len(b) < flyoverLen {
+ return serrors.New("buffer for FlyoverHopField too short", "expected",
+ flyoverLen, "actual", len(b))
+ }
+ b[0] |= 0x80
+ binary.BigEndian.PutUint32(b[12:16], h.ResID<<10+uint32(h.Bw))
+ binary.BigEndian.PutUint16(b[16:18], h.ResStartTime)
+ binary.BigEndian.PutUint16(b[18:20], h.Duration)
+ }
+
+ return nil
+}
diff --git a/pkg/slayers/path/hummingbird/flyoverhopfield_test.go b/pkg/slayers/path/hummingbird/flyoverhopfield_test.go
new file mode 100644
index 0000000000..66c7a9a25c
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/flyoverhopfield_test.go
@@ -0,0 +1,72 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+func TestFlyoverHopSerializeDecodeFlyover(t *testing.T) {
+ expected := &hummingbird.FlyoverHopField{
+ HopField: path.HopField{
+ IngressRouterAlert: true,
+ EgressRouterAlert: true,
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ Flyover: true,
+ ResID: 782,
+ Bw: 23,
+ ResStartTime: 233,
+ Duration: 11,
+ }
+ buf := make([]byte, hummingbird.ExportedFlyoverLen)
+ assert.NoError(t, expected.SerializeTo(buf))
+
+ got := &hummingbird.FlyoverHopField{}
+ assert.NoError(t, got.DecodeFromBytes(buf))
+ assert.Equal(t, expected, got)
+}
+
+func TestFlyoverHopSerializeDecode(t *testing.T) {
+ expected := &hummingbird.FlyoverHopField{
+ HopField: path.HopField{
+ IngressRouterAlert: true,
+ EgressRouterAlert: false,
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ Flyover: false,
+ ResID: 0,
+ Bw: 0,
+ ResStartTime: 0,
+ Duration: 0,
+ }
+ buf := make([]byte, hummingbird.ExportedFlyoverLen)
+ assert.NoError(t, expected.SerializeTo(buf))
+
+ got := &hummingbird.FlyoverHopField{}
+ assert.NoError(t, got.DecodeFromBytes(buf))
+ assert.Equal(t, expected, got)
+}
diff --git a/pkg/slayers/path/hummingbird/mac.go b/pkg/slayers/path/hummingbird/mac.go
new file mode 100644
index 0000000000..4c89c29b3a
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/mac.go
@@ -0,0 +1,120 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build amd64 || arm64 || ppc64 || ppc64le
+
+package hummingbird
+
+import (
+ "crypto/cipher"
+ "encoding/binary"
+
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/slayers/path"
+)
+
+// The FullFlyoverMac makes use of the assembly code in the asm_* files
+// There are two main, related, reasons for that.
+// First, the AES key expansion performed by these assembly files is
+// much faster than what the library code does.
+// BenchmarkFlyoverMac and BenchmarkFlyoverMacLib in mac_test.go show the difference
+//
+// Second, the library implementation of the AES key expansion performs calls to make()
+// and allocates memory, which we would like to avoid
+// This is also the main reason why the direct call to assembly is much faster
+//
+// A full implementation of AES written in go only without memory allocations
+// has been attempted, but turned out to not be much more efficient than
+// the library implementation.
+// This is expectedt to be due to the fact that a go only implementation of AES
+// is unable to make use of hardware accelerated AES instructions.
+
+// defined in asm_* assembly files
+
+//go:noescape
+func encryptBlockAsm(nr int, xk *uint32, dst, src *byte)
+
+//go:noescape
+func expandKeyAsm(nr int, key *byte, enc *uint32)
+
+const (
+ PathType = 5
+
+ aesRounds = 10
+ AkBufferSize = 16
+ FlyoverMacBufferSize = 16
+ XkBufferSize = (aesRounds + 1) * (128 / 32) // 44
+ // Total MAC buffer size:
+ MACBufferSize = path.MACBufferSize + FlyoverMacBufferSize + AkBufferSize
+)
+
+// Derive authentication key A_k
+// block is expected to be initialized beforehand with aes.NewCipher(sv),
+// where sv is this AS' secret value
+// Requires buffer to be of size at least AkBufferSize
+func DeriveAuthKey(
+ block cipher.Block,
+ resId uint32,
+ bw uint16,
+ in uint16,
+ eg uint16,
+ startTime uint32,
+ resDuration uint16,
+ buffer []byte,
+) []byte {
+
+ // Bounds check.
+ _ = buffer[AkBufferSize-1]
+
+ // Prepare input buffer.
+ binary.BigEndian.PutUint16(buffer[0:2], in)
+ binary.BigEndian.PutUint16(buffer[2:4], eg)
+ binary.BigEndian.PutUint32(buffer[4:8], resId<<10|uint32(bw))
+ binary.BigEndian.PutUint32(buffer[8:12], startTime)
+ binary.BigEndian.PutUint16(buffer[12:14], resDuration)
+ binary.BigEndian.PutUint16(buffer[14:16], 0) //padding
+
+ // Should XOR input with iv, but we use iv = 0 => identity
+ block.Encrypt(buffer[0:16], buffer[0:16])
+ return buffer[0:AkBufferSize]
+}
+
+// Computes full flyover MAC Vk based on authentication key Ak.
+// Requires buffer to be of size at least FlyoverMacBufferSize
+// Requires xkbuffer to be of size at least XkBufferSize.
+// (Used to store the AES expanded keys)
+func FullFlyoverMac(
+ ak []byte,
+ dstIA addr.IA,
+ pktlen uint16,
+ resStartTime uint16,
+ highResTime uint32,
+ buffer []byte,
+ xkbuffer []uint32,
+) []byte {
+
+ // Bounds check.
+ _ = buffer[FlyoverMacBufferSize-1]
+ _ = xkbuffer[XkBufferSize-1]
+
+ binary.BigEndian.PutUint64(buffer[0:8], uint64(dstIA))
+ binary.BigEndian.PutUint16(buffer[8:10], pktlen)
+ binary.BigEndian.PutUint16(buffer[10:12], resStartTime)
+ binary.BigEndian.PutUint32(buffer[12:16], highResTime)
+
+ expandKeyAsm(aesRounds, &ak[0], &xkbuffer[0])
+ encryptBlockAsm(aesRounds, &xkbuffer[0], &buffer[0], &buffer[0])
+
+ return buffer[0:FlyoverMacBufferSize]
+}
diff --git a/pkg/slayers/path/hummingbird/mac_test.go b/pkg/slayers/path/hummingbird/mac_test.go
new file mode 100644
index 0000000000..a38adc9292
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/mac_test.go
@@ -0,0 +1,198 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/binary"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+func TestDeriveAuthKey(t *testing.T) {
+ sv := []byte{
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ }
+ var resId uint32 = 0x40
+ var bw uint16 = 0x0203
+ buffer := make([]byte, 16)
+ var in uint16 = 2
+ var eg uint16 = 5
+ var start uint32 = 0x0030001
+ var duration uint16 = 0x0203
+
+ block, err := aes.NewCipher(sv)
+ require.NoError(t, err)
+
+ // Compute expected result with library CBC.
+ expected := make([]byte, hummingbird.AkBufferSize)
+ binary.BigEndian.PutUint16(expected[0:2], in)
+ binary.BigEndian.PutUint16(expected[2:4], eg)
+ binary.BigEndian.PutUint32(expected[4:8], resId<<10)
+ expected[6] |= byte(bw >> 8)
+ expected[7] = byte(bw)
+ binary.BigEndian.PutUint32(expected[8:12], start)
+ binary.BigEndian.PutUint16(expected[12:14], duration)
+ binary.BigEndian.PutUint16(expected[14:16], 0)
+
+ var ZeroBlock [aes.BlockSize]byte
+ mode := cipher.NewCBCEncrypter(block, ZeroBlock[:])
+ mode.CryptBlocks(expected, expected)
+
+ // Check DeriveAuthKey Function.
+ block, err = aes.NewCipher(sv)
+ require.NoError(t, err)
+ key := hummingbird.DeriveAuthKey(block, resId, bw, in, eg, start, duration, buffer)
+ require.Equal(t, expected, key)
+ // Repeat derivation, should yield the same result.
+ key = hummingbird.DeriveAuthKey(block, resId, bw, in, eg, start, duration, buffer)
+ require.Equal(t, expected, key)
+}
+
+func BenchmarkDeriveAuthKey(b *testing.B) {
+ sv := []byte{
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ }
+ var resId uint32 = 0x40
+ var bw uint16 = 0x0203
+ buffer := make([]byte, hummingbird.AkBufferSize)
+ var in uint16 = 2
+ var eg uint16 = 5
+ var start uint32 = 0x0030001
+ var duration uint16 = 0x0203
+
+ block, err := aes.NewCipher(sv)
+ require.NoError(b, err)
+
+ for b.Loop() {
+ hummingbird.DeriveAuthKey(block, resId, bw, in, eg, start, duration, buffer)
+ }
+}
+
+// BenchmarkDeriveAuthKeyManually benchmarks obtaining Ak by just using the stdlib.
+// Results in my machine of 5.987 ns/op.
+// Does not take into account the process of moving data into the buffer
+func BenchmarkDeriveAuthKeyManually(b *testing.B) {
+ sv := []byte{
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ }
+ var resId uint32 = 0x40
+ var bw uint16 = 0x0203
+ var in uint16 = 2
+ var eg uint16 = 5
+ var start uint16 = 0x0001
+ var end uint16 = 0x0203
+ inData := make([]byte, hummingbird.AkBufferSize)
+
+ buffer := make([]byte, hummingbird.AkBufferSize)
+ block, err := aes.NewCipher(sv)
+ require.NoError(b, err)
+
+ b.ResetTimer()
+ for b.Loop() {
+ binary.BigEndian.PutUint32(inData[0:4], resId<<10)
+ inData[2] |= byte(bw >> 8)
+ inData[3] = byte(bw)
+ binary.BigEndian.PutUint16(inData[4:6], in)
+ binary.BigEndian.PutUint16(inData[6:8], eg)
+ binary.BigEndian.PutUint16(inData[8:10], start)
+ binary.BigEndian.PutUint16(inData[10:12], end)
+ binary.BigEndian.PutUint32(inData[12:16], 0) //padding
+ block.Encrypt(buffer[:], inData)
+ }
+}
+
+// We use CBC-MAC using aes for the flyover mac.
+func TestFlyoverMac(t *testing.T) {
+ ak := []byte{
+ 0x7e, 0x61, 0x04, 0x91, 0x30, 0x6b, 0x95, 0xec,
+ 0xb5, 0x75, 0xc6, 0xe9, 0x4c, 0x5a, 0x89, 0x84,
+ }
+ var dstIA addr.IA = 326
+ var pktlen uint16 = 23
+ var resStartTs uint16 = 1234
+ var highResTs uint32 = 4321
+ buffer := make([]byte, hummingbird.FlyoverMacBufferSize)
+ xkbuffer := make([]uint32, hummingbird.XkBufferSize)
+
+ // Compute expected output based on library cbc-mac implementation.
+ expected := make([]byte, hummingbird.FlyoverMacBufferSize)
+ binary.BigEndian.PutUint64(expected[0:8], uint64(dstIA))
+ binary.BigEndian.PutUint16(expected[8:10], pktlen)
+ binary.BigEndian.PutUint16(expected[10:12], resStartTs)
+ binary.BigEndian.PutUint32(expected[12:16], highResTs)
+ block, err := aes.NewCipher(ak)
+ require.NoError(t, err)
+ block.Encrypt(expected[:], expected[:])
+
+ mac := hummingbird.FullFlyoverMac(ak, dstIA, pktlen, resStartTs, highResTs, buffer, xkbuffer)
+ require.Equal(t, expected, mac)
+ // Repeat, to ensure that the result is the same despite using the same xk buffer.
+ mac = hummingbird.FullFlyoverMac(ak, dstIA, pktlen, resStartTs, highResTs, buffer, xkbuffer)
+ require.Equal(t, expected, mac)
+}
+
+func BenchmarkFlyoverMac(b *testing.B) {
+ ak := []byte{
+ 0x7e, 0x61, 0x04, 0x91, 0x30, 0x6b, 0x95, 0xec,
+ 0xb5, 0x75, 0xc6, 0xe9, 0x4c, 0x5a, 0x89, 0x84,
+ }
+ var dstIA addr.IA = 326
+ var pktlen uint16 = 23
+ var resStartTs uint16 = 1234
+ var highResTs uint32 = 4321
+ buffer := make([]byte, hummingbird.FlyoverMacBufferSize)
+ xkbuffer := make([]uint32, hummingbird.XkBufferSize)
+
+ b.ResetTimer()
+ for b.Loop() {
+ hummingbird.FullFlyoverMac(ak, dstIA, pktlen, resStartTs, highResTs, buffer, xkbuffer)
+ }
+}
+
+// Benchmark of the Flyover MAC if we use library code only
+// Without using the assembly code in the asm_* files.
+func BenchmarkFlyoverMacLib(b *testing.B) {
+ ak := []byte{
+ 0x7e, 0x61, 0x04, 0x91, 0x30, 0x6b, 0x95, 0xec,
+ 0xb5, 0x75, 0xc6, 0xe9, 0x4c, 0x5a, 0x89, 0x84,
+ }
+
+ var dstIA addr.IA = 326
+ var pktlen uint16 = 23
+ var resStartTs uint16 = 1234
+ var highResTs uint32 = 4321
+ buffer := make([]byte, hummingbird.FlyoverMacBufferSize)
+
+ // Compute expected output based on library cbc-mac implementation.
+ b.ResetTimer()
+ for b.Loop() {
+ binary.BigEndian.PutUint64(buffer[0:8], uint64(dstIA))
+ binary.BigEndian.PutUint16(buffer[8:10], pktlen)
+ binary.BigEndian.PutUint16(buffer[10:12], resStartTs)
+ binary.BigEndian.PutUint32(buffer[12:16], highResTs)
+
+ block, _ := aes.NewCipher(ak)
+ block.Encrypt(buffer[:], buffer[:])
+ }
+}
diff --git a/pkg/slayers/path/hummingbird/raw.go b/pkg/slayers/path/hummingbird/raw.go
new file mode 100644
index 0000000000..2c4c9ceadb
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/raw.go
@@ -0,0 +1,382 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird
+
+import (
+ "encoding/binary"
+
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/slayers/path"
+)
+
+// Raw is a raw representation of the Hummingbird (data-plane) path type. It is designed to parse as
+// little as possible and should be used if performance matters.
+type Raw struct {
+ Base
+ Raw []byte
+}
+
+// DecodeFromBytes only decodes the PathMetaHeader. Otherwise the nothing is decoded and simply kept
+// as raw bytes.
+func (s *Raw) DecodeFromBytes(data []byte) error {
+ if err := s.Base.DecodeFromBytes(data); err != nil {
+ return err
+ }
+ pathLen := s.Len()
+ if len(data) < pathLen {
+ return serrors.New("RawPath raw too short", "expected", pathLen, "actual", len(data))
+ }
+ s.Raw = data[:pathLen]
+ return nil
+}
+
+// SerializeTo writes the path to a slice. The slice must be big enough to hold the entire data,
+// otherwise an error is returned.
+func (s *Raw) SerializeTo(b []byte) error {
+ if s.Raw == nil {
+ return serrors.New("raw is nil")
+ }
+ if minLen := s.Len(); len(b) < minLen {
+ return serrors.New("buffer too small", "expected", minLen, "actual", len(b))
+ }
+ // XXX(roosd): This modifies the underlying buffer. Consider writing to data
+ // directly.
+ if err := s.PathMeta.SerializeTo(s.Raw[:MetaLen]); err != nil {
+ return err
+ }
+
+ copy(b, s.Raw)
+ return nil
+}
+
+// Reverse reverses the path such that it can be used in the reverse direction.
+// Removes all flyovers in the process
+func (s *Raw) Reverse() (path.Path, error) {
+ // XXX(shitz): The current implementation is not the most performant, since it parses the entire
+ // path first. If this becomes a performance bottleneck, the implementation should be changed to
+ // work directly on the raw representation.
+
+ decoded, err := s.ToDecoded()
+ if err != nil {
+ return nil, err
+ }
+ reversed, err := decoded.Reverse()
+ if err != nil {
+ return nil, err
+ }
+ if err := reversed.SerializeTo(s.Raw); err != nil {
+ return nil, err
+ }
+ err = s.DecodeFromBytes(s.Raw)
+ return s, err
+}
+
+// ToDecoded transforms a hummingbird.Raw to a hummingbird.Decoded.
+func (s *Raw) ToDecoded() (*Decoded, error) {
+ // Serialize PathMeta to ensure potential changes are reflected Raw.
+
+ if err := s.PathMeta.SerializeTo(s.Raw[:MetaLen]); err != nil {
+ return nil, err
+ }
+
+ decoded := &Decoded{}
+ if err := decoded.DecodeFromBytes(s.Raw); err != nil {
+ return nil, err
+ }
+ return decoded, nil
+}
+
+// IncPath increments the path by n and writes it to the buffer.
+func (s *Raw) IncPath(n int) error {
+ if err := s.Base.IncPath(n); err != nil {
+ return err
+ }
+
+ return s.PathMeta.SerializeTo(s.Raw[:MetaLen])
+}
+
+// GetInfoField returns the InfoField at a given index.
+func (s *Raw) GetInfoField(idx int) (path.InfoField, error) {
+ if idx >= s.NumINF {
+ return path.InfoField{},
+ serrors.New("InfoField index out of bounds", "max", s.NumINF-1, "actual", idx)
+ }
+ infOffset := MetaLen + idx*path.InfoLen
+ info := path.InfoField{}
+ if err := info.DecodeFromBytes(s.Raw[infOffset : infOffset+path.InfoLen]); err != nil {
+ return path.InfoField{}, err
+ }
+ return info, nil
+}
+
+// GetCurrentInfoField is a convenience method that returns the current info field pointed to by the
+// CurrINF index in the path meta header.
+func (s *Raw) GetCurrentInfoField() (path.InfoField, error) {
+ return s.GetInfoField(int(s.PathMeta.CurrINF))
+}
+
+// Returns whether the hopfield at the given index is in construction direction
+func (s *Raw) isConsdir(idx uint8) bool {
+ hopIdx := s.Base.InfIndexForHF(idx)
+ infOffset := MetaLen + hopIdx*path.InfoLen
+ return s.Raw[infOffset]&0x1 == 0x1
+
+}
+
+// SetInfoField updates the InfoField at a given index.
+func (s *Raw) SetInfoField(info path.InfoField, idx int) error {
+ if idx >= s.NumINF {
+ return serrors.New("InfoField index out of bounds", "max", s.NumINF-1, "actual", idx)
+ }
+ infOffset := MetaLen + idx*path.InfoLen
+ return info.SerializeTo(s.Raw[infOffset : infOffset+path.InfoLen])
+}
+
+// GetHopField returns the HopField beginning at a given index.
+// Does NOT check whether the given index is the first line of a hopfield
+// Responsibility to check that falls to the caller
+func (s *Raw) GetHopField(idx int) (FlyoverHopField, error) {
+ if idx >= s.NumLines-HopLines+1 {
+ return FlyoverHopField{},
+ serrors.New("HopField index out of bounds", "max", s.NumLines-HopLines, "actual", idx)
+ }
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ hop := FlyoverHopField{}
+ // Let the decoder read a big enough slice in case it is a FlyoverHopField
+ maxHopLen := flyoverLen
+ if idx > s.NumLines-FlyoverLines {
+ if idx == s.NumLines-HopLines {
+ maxHopLen = hopLen
+ } else {
+ return FlyoverHopField{}, serrors.New(
+ "Invalid hopfield index", "NumHops", s.NumLines, "index", idx)
+ }
+ }
+ if err := hop.DecodeFromBytes(s.Raw[hopOffset : hopOffset+maxHopLen]); err != nil {
+ return FlyoverHopField{}, err
+ }
+ return hop, nil
+}
+
+// GetCurrentHopField is a convenience method that returns the current hop field pointed to by the
+// CurrHF index in the path meta header.
+func (s *Raw) GetCurrentHopField() (FlyoverHopField, error) {
+ return s.GetHopField(int(s.PathMeta.CurrHF))
+}
+
+// ReplaceMac replaces the Mac of the hopfield at the given index with a new MAC.
+func (s *Raw) ReplacMac(idx int, mac []byte) error {
+ if idx >= s.NumLines-HopLines+1 {
+ return serrors.New("HopField index out of bounds", "max",
+ s.NumLines-HopLines, "actual", idx)
+ }
+ offset := s.NumINF*path.InfoLen + MetaLen + idx*LineLen + macOffset
+ if n := copy(s.Raw[offset:offset+path.MacLen], mac[:path.MacLen]); n != path.MacLen {
+ return serrors.New("copied worng number of bytes for mac replacement",
+ "expected", path.MacLen, "actual", n)
+ }
+ return nil
+}
+
+// SetCurrentMac replaces the Mac of the current hopfield by a new MAC.
+func (s *Raw) ReplaceCurrentMac(mac []byte) error {
+ return s.ReplacMac(int(s.PathMeta.CurrHF), mac)
+}
+
+// Returns a slice of the MAC of the hopfield starting at index idx
+// It is the caller's responsibility to make sure line idx is the beginning of a hopfield.
+func (s *Raw) GetMac(idx int) ([]byte, error) {
+ if idx >= s.NumLines-HopLines+1 {
+ return nil, serrors.New("HopField index out of bounds",
+ "max", s.NumLines-HopLines, "actual", idx)
+ }
+ offset := s.NumINF*path.InfoLen + MetaLen + idx*LineLen + macOffset
+ return s.Raw[offset : offset+path.MacLen], nil
+}
+
+// SetHopField updates the HopField at a given index.
+// For Hummingbird paths the index is the offset in 4 byte lines
+//
+// If replacing a FlyoverHopField with a Hopfield,
+// it is replaced by a FlyoverHopField with dummy values.
+// This works for SCMP packets as Flyover hops are removed later
+// in the process of building a SCMP packet.
+//
+// Does not allow replacing a normal hopfield with a FlyoverHopField.
+func (s *Raw) SetHopField(hop FlyoverHopField, idx int) error {
+ if idx >= s.NumLines-HopLines+1 {
+ return serrors.New("HopField index out of bounds",
+ "max", s.NumLines-HopLines, "actual", idx)
+ }
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ if s.Raw[hopOffset]&0x80 == 0x80 {
+ // If the current hop is a flyover, the flyover bit of the new hop is set to 1
+ // in order to preserve correctness of the path.
+ //
+ // The reservation data of the new hop is dummy data and invalid.
+ // This works because SetHopField is currently only used to prepare a SCMP packet,
+ // and all flyovers are removed later in that process.
+ //
+ // If this is ever used for something else, this function needs to be re-written.
+ hop.Flyover = true
+ }
+ if hop.Flyover {
+ if idx >= s.NumLines-FlyoverLines+1 {
+ return serrors.New("FlyoverHopField index out of bounds",
+ "max", s.NumLines-FlyoverLines, "actual", idx)
+ }
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ if s.Raw[hopOffset]&0x80 == 0x00 {
+ return serrors.New(
+ "Setting FlyoverHopField over Hopfield with setHopField not supported")
+ }
+ return hop.SerializeTo(s.Raw[hopOffset : hopOffset+flyoverLen])
+ }
+ return hop.SerializeTo(s.Raw[hopOffset : hopOffset+hopLen])
+}
+
+// IsFirstHop returns whether the current hop is the first hop on the path.
+func (s *Raw) IsFirstHop() bool {
+ return s.PathMeta.CurrHF == 0
+}
+
+// IsLastHop returns whether the current hop is the last hop on the path.
+func (s *Raw) IsLastHop() bool {
+ return int(s.PathMeta.CurrHF) == (s.NumLines-HopLines) ||
+ int(s.PathMeta.CurrHF) == (s.NumLines-FlyoverLines)
+}
+
+// Returns the egress interface of the next hop.
+func (s *Raw) GetNextEgress() (uint16, error) {
+ idx := int(s.Base.PathMeta.CurrHF)
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ if s.Raw[hopOffset]&0x80 == 0x80 {
+ idx += FlyoverLines
+ hopOffset += FlyoverLines * LineLen
+ } else {
+ idx += HopLines
+ hopOffset += HopLines * LineLen
+ }
+ if idx >= s.NumLines-2 {
+ return 0, serrors.New("HopField index out of bounds", "max",
+ s.NumLines-HopLines, "actual", idx)
+ }
+ if s.isConsdir(uint8(idx)) {
+ return binary.BigEndian.Uint16(s.Raw[hopOffset+4 : hopOffset+6]), nil
+ }
+ return binary.BigEndian.Uint16(s.Raw[hopOffset+2 : hopOffset+4]), nil
+}
+
+// Returns the ingress interface of the previous hop
+// Does NOT work if the previous hop is a flyover hop.
+func (s *Raw) GetPreviousIngress() (uint16, error) {
+ idx := int(s.Base.PathMeta.CurrHF) - HopLines
+ if idx < 0 {
+ return 0, serrors.New("HopField index out of bounds", "min", 0, "actual", idx)
+ }
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ if s.isConsdir(uint8(idx)) {
+ return binary.BigEndian.Uint16(s.Raw[hopOffset+2 : hopOffset+4]), nil
+ }
+ return binary.BigEndian.Uint16(s.Raw[hopOffset+4 : hopOffset+6]), nil
+}
+
+// MoveFlyoverToNext attaches previous flyoverfield to current hopfield.
+// DOES NOT adapt MACs.
+// Assumes previous hopfield has a flyover.
+// Assumes to be the first hop of the second or third segment.
+// If a flyover is depicted like this:
+//
+// Line index: 0 1 2 3 4 5
+// | hop |fly|
+//
+// Graphically, the byte buffer is transformed as follows:
+//
+// -5 4 3 2 1 0 1 2 3 -5 4 3 2 1 0 1 2 3
+// | flyovr1 | hop2| gets morphed into: | hop1| flyovr2 |
+//
+// Then the pointer to the current field is modified by substracting 2,
+// as it starts now two lines before the original CurrHF.
+// Because only the flyover part is copied, the fields ResID, BW, ResStartOffset, and Duration are
+// correct, but not the MACs of the current or previous hop fields:
+// - The previous hop field contains an aggregated MAC, but is no longer a flyover.
+// - The current hop field contains only a MAC, but as a flyover it requires an aggregated MAC.
+func (s *Raw) MoveFlyoverToNext() error {
+ idx := int(s.Base.PathMeta.CurrHF)
+ if idx >= s.NumLines-2 {
+ return serrors.New("CurrHF out of bounds",
+ "max", s.NumLines-2, "actual", idx)
+ }
+ prevHopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen - flyoverLen
+ buff := s.Raw[prevHopOffset:] // buff points to the beginning of the previous hop
+ if buff[flyoverLen]&0x80 == 0x80 {
+ return serrors.New("Current hop does already have a Flyover")
+ }
+ // buffer flyover and copy data
+ var temp [2 * LineLen]byte
+ copy(
+ temp[:],
+ buff[hopLen:flyoverLen]) // save the flyover-only part (2 lines)
+ copy(
+ buff[hopLen:2*hopLen],
+ buff[flyoverLen:flyoverLen+hopLen]) // copy the current hopfield in place (3 lines)
+ copy(
+ buff[2*hopLen:hopLen+flyoverLen],
+ temp[:]) // copy back the flyover-only part, to the current hop
+
+ // Unset and Set Flyoverbits
+ buff[0] &= 0x7f // Unset MSBit, flyover == false.
+ buff[hopLen] |= 0x80 // Set MSbit, flyover == true.
+ // Adapt seglens
+ s.Base.PathMeta.CurrHF -= 2
+ s.Base.PathMeta.SegLen[s.PathMeta.CurrINF-1] -= 2
+ s.Base.PathMeta.SegLen[s.PathMeta.CurrINF] += 2
+ return s.Base.PathMeta.SerializeTo(s.Raw[:])
+}
+
+// Attaches current flyoverfield to previous hopfield
+// DOES NOT adapt MACs.
+// It is assumed that the previous hopfield does NOT already have a flyover.
+func (s *Raw) MoveFlyoverToPrevious() error {
+ idx := int(s.Base.PathMeta.CurrHF)
+ if idx < 6 {
+ return serrors.New("CurrHF too small for reversing flyover crossover",
+ "min", 6, "actual", idx)
+ }
+ if s.PathMeta.CurrINF == 0 {
+ return serrors.New("Cannot reverse Flyover Xover when CurrINF = 0")
+ }
+ hopOffset := MetaLen + s.NumINF*path.InfoLen + idx*LineLen
+ if s.Raw[hopOffset]&0x80 == 0x00 {
+ return serrors.New("Current hop does not have a Flyover")
+ }
+ if s.Raw[hopOffset-hopLen]&0x80 != 0x00 {
+ return serrors.New(
+ "Cannot Reverse Flyover Crossover, flyover bit set where previous hop should be")
+ }
+ var t [flyoverLen - hopLen]byte
+ copy(t[:], s.Raw[hopOffset+hopLen:hopOffset+flyoverLen])
+ copy(s.Raw[hopOffset+flyoverLen-hopLen:hopOffset+flyoverLen],
+ s.Raw[hopOffset:hopOffset+hopLen])
+ copy(s.Raw[hopOffset:hopOffset+flyoverLen-hopLen], t[:])
+ // Set and Unset Flyoverbits
+ s.Raw[hopOffset-hopLen] |= 0x80
+ s.Raw[hopOffset+flyoverLen-hopLen] &= 0x7f
+ // Adapt Seglens and CurrHF
+ s.Base.PathMeta.SegLen[s.PathMeta.CurrINF] -= 2
+ s.Base.PathMeta.SegLen[s.PathMeta.CurrINF-1] += 2
+ s.Base.PathMeta.CurrHF += 2
+ return s.Base.PathMeta.SerializeTo(s.Raw[:])
+}
diff --git a/pkg/slayers/path/hummingbird/raw_test.go b/pkg/slayers/path/hummingbird/raw_test.go
new file mode 100644
index 0000000000..68a9537812
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/raw_test.go
@@ -0,0 +1,261 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+var emptyRawTestPath = &hummingbird.Raw{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 0,
+ SegLen: [3]uint8{0, 0, 0},
+ },
+ NumINF: 0,
+ NumLines: 0,
+ },
+ Raw: make([]byte, hummingbird.MetaLen),
+}
+
+var rawHbirdTestPath = &hummingbird.Raw{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 0,
+ SegLen: [3]uint8{8, 8, 0},
+ BaseTS: 808,
+ HighResTS: 1234,
+ },
+ NumINF: 2,
+ NumLines: 16,
+ },
+ Raw: decodedBytes[0],
+}
+
+func TestRawSerializeHbird(t *testing.T) {
+ b := make([]byte, rawHbirdTestPath.Len())
+ assert.NoError(t, rawHbirdTestPath.SerializeTo(b))
+ assert.Equal(t, decodedBytes[0], b)
+}
+
+func TestRawDecodeFromBytesHbird(t *testing.T) {
+ s := &hummingbird.Raw{}
+ assert.NoError(t, s.DecodeFromBytes(decodedBytes[0]))
+ assert.Equal(t, rawHbirdTestPath, s)
+}
+
+func TestRawSerializeDecodeHbird(t *testing.T) {
+ b := make([]byte, rawHbirdTestPath.Len())
+ assert.NoError(t, rawHbirdTestPath.SerializeTo(b))
+ s := &hummingbird.Raw{}
+ assert.NoError(t, s.DecodeFromBytes(b))
+ assert.Equal(t, rawHbirdTestPath, s)
+}
+
+func TestRawReverseHbird(t *testing.T) {
+ for name, tc := range pathReverseTestCases {
+ name, tc := name, tc
+ for i := range tc.inIdxs {
+ i := i
+ t.Run(fmt.Sprintf("%s case %d", name, i+1), func(t *testing.T) {
+ t.Parallel()
+ input := mkRawHbirdPath(t, tc.input, uint8(tc.inIdxs[i][0]),
+ uint8(tc.inIdxs[i][1]))
+ want := mkRawHbirdPath(t, tc.want, uint8(tc.wantIdxs[i][0]),
+ uint8(tc.wantIdxs[i][1]))
+ revPath, err := input.Reverse()
+ assert.NoError(t, err)
+ assert.Equal(t, want, revPath)
+ })
+ }
+ }
+}
+
+func TestEmptyRawReverse(t *testing.T) {
+ _, err := emptyRawTestPath.Reverse()
+ assert.Error(t, err)
+}
+
+func TestRawToDecodedHbird(t *testing.T) {
+ got, err := rawHbirdTestPath.ToDecoded()
+ assert.NoError(t, err)
+ assert.Equal(t, decodedPaths[0], got)
+}
+
+func TestGetInfoField(t *testing.T) {
+ testCases := map[string]struct {
+ idx int
+ want path.InfoField
+ errorFunc assert.ErrorAssertionFunc
+ }{
+ "first info": {
+ idx: 0,
+ want: infoFields[0],
+ errorFunc: assert.NoError,
+ },
+ "second info": {
+ idx: 1,
+ want: infoFields[1],
+ errorFunc: assert.NoError,
+ },
+ "out of bounds": {
+ idx: 2,
+ want: path.InfoField{},
+ errorFunc: assert.Error,
+ },
+ }
+
+ for name, tc := range testCases {
+ name, tc := name, tc
+
+ t.Run(name+" hummingbird", func(t *testing.T) {
+ t.Parallel()
+ got, err := rawHbirdTestPath.GetInfoField(tc.idx)
+ tc.errorFunc(t, err)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func TestGetHbirdHopField(t *testing.T) {
+ testCases := map[string]struct {
+ idx int
+ want hummingbird.FlyoverHopField
+ errorFunc assert.ErrorAssertionFunc
+ }{
+ "first hop": {
+ idx: 0,
+ want: flyoverFields[0],
+ errorFunc: assert.NoError,
+ },
+ "fourth hop": {
+ idx: 11,
+ want: flyoverFields[3],
+ errorFunc: assert.NoError,
+ },
+ "invalid index": {
+ idx: 12,
+ errorFunc: assert.Error,
+ },
+ "out of bounds": {
+ idx: 14,
+ errorFunc: assert.Error,
+ },
+ }
+
+ for name, tc := range testCases {
+ name, tc := name, tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ got, err := rawHbirdTestPath.GetHopField(tc.idx)
+ tc.errorFunc(t, err)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func TestLastHop(t *testing.T) {
+ testCases := map[*hummingbird.Raw]bool{
+ createHbirdPath(3, 9): false,
+ createHbirdPath(3, 11): false,
+ createHbirdPath(3, 12): false,
+ createHbirdPath(6, 9): true,
+ createHbirdPath(6, 11): true,
+ }
+ for scionRaw, want := range testCases {
+ got := scionRaw.IsLastHop()
+ assert.Equal(t, want, got)
+ }
+}
+
+func TestSetHopfield(t *testing.T) {
+ hop1 := hummingbird.FlyoverHopField{
+ HopField: path.HopField{
+ ConsIngress: 0,
+ ConsEgress: 1,
+ },
+ }
+ hop2 := hummingbird.FlyoverHopField{
+ HopField: path.HopField{
+ ConsIngress: 2,
+ ConsEgress: 3,
+ },
+ }
+ hop3 := hummingbird.FlyoverHopField{
+ Flyover: true,
+ HopField: path.HopField{
+ ConsIngress: 1,
+ ConsEgress: 0,
+ },
+ ResID: 13,
+ Bw: 6,
+ ResStartTime: 0,
+ Duration: 45,
+ }
+ expected := decodedPaths[0]
+ expected.HopFields[0] = hop1
+ expected.HopFields[0].Flyover = true
+ expected.HopFields[1] = hop2
+ expected.HopFields[3] = hop3
+
+ buffer := make([]byte, expected.Len())
+ err := expected.SerializeTo(buffer)
+ assert.NoError(t, err)
+
+ testPath := rawHbirdTestPath
+
+ err = testPath.SetHopField(hop1, 0)
+ assert.NoError(t, err)
+
+ err = testPath.SetHopField(hop2, 5)
+ assert.NoError(t, err)
+
+ err = testPath.SetHopField(hop3, 11)
+ assert.NoError(t, err)
+
+ result, err := testPath.ToDecoded()
+
+ assert.NoError(t, err)
+ require.Equal(t, expected, result)
+}
+
+func mkRawHbirdPath(t *testing.T, pcase hbirdPathCase, infIdx, hopIdx uint8) *hummingbird.Raw {
+ t.Helper()
+ decoded := mkDecodedHbirdPath(t, pcase, infIdx, hopIdx)
+ raw, err := decoded.ToRaw()
+ require.NoError(t, err)
+ return raw
+}
+
+func createHbirdPath(currHF uint8, numHops int) *hummingbird.Raw {
+ hbirdRaw := &hummingbird.Raw{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: currHF,
+ },
+ NumLines: numHops,
+ },
+ }
+ return hbirdRaw
+}
diff --git a/pkg/slayers/path/hummingbird/testpaths_test.go b/pkg/slayers/path/hummingbird/testpaths_test.go
new file mode 100644
index 0000000000..a807a0a911
--- /dev/null
+++ b/pkg/slayers/path/hummingbird/testpaths_test.go
@@ -0,0 +1,229 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hummingbird_test
+
+import (
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+var infoFields = []path.InfoField{
+ {
+ Peer: false,
+ ConsDir: false,
+ SegID: 0x111,
+ Timestamp: 0x100,
+ },
+ {
+ Peer: false,
+ ConsDir: true,
+ SegID: 0x222,
+ Timestamp: 0x100,
+ },
+}
+
+var flyoverFields = []hummingbird.FlyoverHopField{
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ Flyover: true,
+ ResID: 0,
+ Bw: 4,
+ ResStartTime: 2,
+ Duration: 1,
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 3,
+ ConsEgress: 2,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 0,
+ ConsEgress: 2,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ Flyover: true,
+ ResID: 0,
+ Bw: 4,
+ ResStartTime: 0,
+ Duration: 1,
+ },
+}
+
+var decodedPaths = []*hummingbird.Decoded{
+ {
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 0,
+ SegLen: [3]uint8{8, 8, 0},
+ BaseTS: 808,
+ HighResTS: 1234,
+ },
+ NumINF: 2,
+ NumLines: 16,
+ },
+ InfoFields: infoFields,
+ HopFields: flyoverFields,
+ FirstHopPerSeg: [2]uint8{2, 4},
+ },
+ {
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 0,
+ SegLen: [3]uint8{8, 6, 0},
+ BaseTS: 808,
+ HighResTS: 1234,
+ },
+ NumINF: 2,
+ NumLines: 14,
+ },
+ InfoFields: infoFields,
+ HopFields: []hummingbird.FlyoverHopField{
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ Flyover: true,
+ ResID: 0,
+ Bw: 4,
+ ResStartTime: 2,
+ Duration: 1,
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 3,
+ ConsEgress: 2,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 0,
+ ConsEgress: 2,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ },
+ {
+ HopField: path.HopField{
+ ExpTime: 63,
+ ConsIngress: 1,
+ ConsEgress: 0,
+ Mac: [path.MacLen]byte{1, 2, 3, 4, 5, 6},
+ },
+ },
+ },
+
+ FirstHopPerSeg: [2]uint8{2, 4},
+ },
+}
+
+var decodedBytes = [][]byte{
+ []byte("\x00\x02\x04\x00\x00\x00\x03\x28\x00\x00\x04\xd2" +
+ "\x00\x00\x01\x11\x00\x00\x01\x00\x01\x00\x02\x22\x00\x00\x01\x00" +
+ "\x80\x3f\x00\x01\x00\x00\x01\x02\x03\x04\x05\x06\x00\x00\x00\x04\x00\x02\x00\x01" +
+ "\x00\x3f\x00\x03\x00\x02\x01\x02\x03\x04\x05\x06" +
+ "\x00\x3f\x00\x00\x00\x02\x01\x02\x03\x04\x05\x06" +
+ "\x80\x3f\x00\x01\x00\x00\x01\x02\x03\x04\x05\x06\x00\x00\x00\x04\x00\x00\x00\x01"),
+
+ []byte("\x00\x02\x03\x00\x00\x00\x03\x28\x00\x00\x04\xd2" +
+ "\x00\x00\x01\x11\x00\x00\x01\x00\x01\x00\x02\x22\x00\x00\x01\x00" +
+ "\x80\x3f\x00\x01\x00\x00\x01\x02\x03\x04\x05\x06\x00\x00\x00\x04\x00\x02\x00\x01" +
+ "\x00\x3f\x00\x03\x00\x02\x01\x02\x03\x04\x05\x06" +
+ "\x00\x3f\x00\x00\x00\x02\x01\x02\x03\x04\x05\x06" +
+ "\x00\x3f\x00\x01\x00\x00\x01\x02\x03\x04\x05\x06"),
+}
+
+var pathReverseTestCases = map[string]struct {
+ input hbirdPathCase
+ want hbirdPathCase
+ inIdxs [][2]int
+ wantIdxs [][2]int
+}{
+ "1 segment, 2 hops": {
+ input: hbirdPathCase{[]bool{true}, [][][]uint16{{{11, 0}, {12, 1}}}},
+ want: hbirdPathCase{[]bool{false}, [][][]uint16{{{12, 0}, {11, 0}}}},
+ inIdxs: [][2]int{{0, 0}, {0, 3}},
+ wantIdxs: [][2]int{{0, 3}, {0, 0}},
+ },
+ "1 segment, 5 hops": {
+ input: hbirdPathCase{[]bool{true},
+ [][][]uint16{{{11, 1}, {12, 1}, {13, 0}, {14, 1}, {15, 0}}}},
+ want: hbirdPathCase{[]bool{false},
+ [][][]uint16{{{15, 0}, {14, 0}, {13, 0}, {12, 0}, {11, 0}}}},
+ inIdxs: [][2]int{{0, 0}, {0, 5}, {0, 10}, {0, 13}, {0, 18}},
+ wantIdxs: [][2]int{{0, 12}, {0, 9}, {0, 6}, {0, 3}, {0, 0}},
+ },
+ "2 segments, 5 hops": {
+ input: hbirdPathCase{[]bool{true, false},
+ [][][]uint16{{{11, 0}, {12, 0}}, {{13, 1}, {14, 1}, {15, 0}}}},
+ want: hbirdPathCase{[]bool{true, false},
+ [][][]uint16{{{15, 0}, {14, 0}, {13, 0}}, {{12, 0}, {11, 0}}}},
+ inIdxs: [][2]int{{0, 0}, {0, 3}, {1, 6}, {1, 11}, {1, 16}},
+ wantIdxs: [][2]int{{1, 12}, {1, 9}, {0, 6}, {0, 3}, {0, 0}},
+ },
+ "3 segments, 9 hops": {
+ input: hbirdPathCase{
+ []bool{true, false, false},
+ [][][]uint16{
+ {{11, 1}, {12, 0}},
+ {{13, 0}, {14, 1}, {15, 1}, {16, 0}},
+ {{17, 0}, {18, 1}, {19, 1}},
+ },
+ },
+ want: hbirdPathCase{
+ []bool{true, true, false},
+ [][][]uint16{
+ {{19, 0}, {18, 0}, {17, 0}},
+ {{16, 0}, {15, 0}, {14, 0}, {13, 0}},
+ {{12, 0}, {11, 0}},
+ },
+ },
+ inIdxs: [][2]int{
+ {0, 0}, {0, 5}, {1, 8}, {1, 11}, {1, 16}, {1, 21}, {2, 24}, {2, 27}, {2, 32},
+ },
+ wantIdxs: [][2]int{
+ {2, 24}, {2, 21}, {1, 18}, {1, 15}, {1, 12}, {1, 9}, {0, 6}, {0, 3}, {0, 0},
+ },
+ },
+}
+
+type hbirdPathCase struct {
+ infos []bool
+ hops [][][]uint16
+}
diff --git a/pkg/slayers/scion.go b/pkg/slayers/scion.go
index 5511d87ea6..137435de62 100644
--- a/pkg/slayers/scion.go
+++ b/pkg/slayers/scion.go
@@ -25,6 +25,7 @@ import (
"github.com/scionproto/scion/pkg/slayers/path"
"github.com/scionproto/scion/pkg/slayers/path/empty"
"github.com/scionproto/scion/pkg/slayers/path/epic"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
"github.com/scionproto/scion/pkg/slayers/path/onehop"
"github.com/scionproto/scion/pkg/slayers/path/scion"
)
@@ -46,6 +47,7 @@ func init() {
scion.RegisterPath()
onehop.RegisterPath()
epic.RegisterPath()
+ hummingbird.RegisterPath()
}
// AddrType indicates the type of a host address in the SCION header.
@@ -262,10 +264,11 @@ func (s *SCION) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error {
func (s *SCION) RecyclePaths() {
if s.pathPool == nil {
s.pathPool = []path.Path{
- empty.PathType: empty.Path{},
- onehop.PathType: &onehop.Path{},
- scion.PathType: &scion.Raw{},
- epic.PathType: &epic.Path{},
+ empty.PathType: empty.Path{},
+ onehop.PathType: &onehop.Path{},
+ scion.PathType: &scion.Raw{},
+ epic.PathType: &epic.Path{},
+ hummingbird.PathType: &hummingbird.Raw{},
}
s.pathPoolRaw = path.NewRawPath()
}
diff --git a/pkg/slayers/scmp_typecode.go b/pkg/slayers/scmp_typecode.go
index eb61d1dcfc..564b7b0fc0 100644
--- a/pkg/slayers/scmp_typecode.go
+++ b/pkg/slayers/scmp_typecode.go
@@ -72,6 +72,8 @@ const (
SCMPCodeInvalidExtensionHeader SCMPCode = 64
SCMPCodeUnknownHopByHopOption SCMPCode = 65
SCMPCodeUnknownEndToEndOption SCMPCode = 66
+
+ SCMPCodeReservationExpired SCMPCode = 71
)
// SCMP informational messages.
diff --git a/router/BUILD.bazel b/router/BUILD.bazel
index 9d42f1a9da..9a21766398 100644
--- a/router/BUILD.bazel
+++ b/router/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
srcs = [
"connector.go",
"dataplane.go",
+ "dataplane_hbird.go",
"doc.go",
"metrics.go",
"serialize_proxy.go",
@@ -28,6 +29,7 @@ go_library(
"//pkg/slayers/path:go_default_library",
"//pkg/slayers/path/empty:go_default_library",
"//pkg/slayers/path/epic:go_default_library",
+ "//pkg/slayers/path/hummingbird:go_default_library",
"//pkg/slayers/path/onehop:go_default_library",
"//pkg/slayers/path/scion:go_default_library",
"//pkg/spao:go_default_library",
@@ -38,6 +40,7 @@ go_library(
"//router/bfd:go_default_library",
"//router/config:go_default_library",
"//router/control:go_default_library",
+ "//router/tokenbucket:go_default_library",
"@com_github_gopacket_gopacket//:go_default_library",
"@com_github_gopacket_gopacket//layers:go_default_library",
"@com_github_prometheus_client_golang//prometheus:go_default_library",
@@ -48,6 +51,7 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
+ "dataplane_hbird_test.go",
"dataplane_internal_test.go",
"dataplane_test.go",
"export_test.go",
@@ -65,6 +69,7 @@ go_test(
"//pkg/slayers/path:go_default_library",
"//pkg/slayers/path/empty:go_default_library",
"//pkg/slayers/path/epic:go_default_library",
+ "//pkg/slayers/path/hummingbird:go_default_library",
"//pkg/slayers/path/onehop:go_default_library",
"//pkg/slayers/path/scion:go_default_library",
"//private/topology:go_default_library",
diff --git a/router/connector.go b/router/connector.go
index 4b8c9916e3..f275ab47a3 100644
--- a/router/connector.go
+++ b/router/connector.go
@@ -183,6 +183,20 @@ func (c *Connector) SetKey(ia addr.IA, index int, key []byte) error {
return c.DataPlane.SetKey(key)
}
+// SetHbirdKey sets the hummingbird key for the given ISD-AS at the given indey
+func (c *Connector) SetHbirdKey(ia addr.IA, index int, sv []byte) error {
+ c.mtx.Lock()
+ defer c.mtx.Unlock()
+ log.Debug("Setting Humingbird secret key", "isd_as", ia, "index", index)
+ if !c.ia.Equal(ia) {
+ return serrors.JoinNoStack(errMultiIA, nil, "current", c.ia, "new", ia)
+ }
+ if index != 0 {
+ return serrors.New("currently only index 0 secret key is supported")
+ }
+ return c.DataPlane.SetHbirdKey(sv)
+}
+
func (c *Connector) ListInternalInterfaces() ([]control.InternalInterface, error) {
c.mtx.Lock()
defer c.mtx.Unlock()
diff --git a/router/control/conf.go b/router/control/conf.go
index f56fb771a6..4c8557fdd2 100644
--- a/router/control/conf.go
+++ b/router/control/conf.go
@@ -39,6 +39,7 @@ type Dataplane interface {
AddSvc(ia addr.IA, svc addr.SVC, a addr.Host, port uint16) error
DelSvc(ia addr.IA, svc addr.SVC, a addr.Host, port uint16) error
SetKey(ia addr.IA, index int, key []byte) error
+ SetHbirdKey(ia addr.IA, index int, key []byte) error
SetPortRange(start, end uint16)
}
@@ -137,6 +138,10 @@ func ConfigDataplane(dp Dataplane, cfg *Config) error {
if err := dp.SetKey(cfg.IA, 0, key0); err != nil {
return err
}
+ keyHbird := deriveHbirdSecretValue(cfg.MasterKeys.Key0)
+ if err := dp.SetHbirdKey(cfg.IA, 0, keyHbird); err != nil {
+ return err
+ }
}
// Add internal interfaces
@@ -297,3 +302,14 @@ func confServices(dp Dataplane, cfg *Config) error {
}
return nil
}
+
+// deriveHbirdSecretValue derives hummingbird AS secret value from the given key
+func deriveHbirdSecretValue(k []byte) []byte {
+ if len(k) == 0 {
+ panic("empty key")
+ }
+ hbirdSalt := []byte("Derive hbird sv")
+ // This uses 16B keys with 1000 hash iterations, which is the same as the
+ // defaults used by pycrypto.
+ return pbkdf2.Key(k, hbirdSalt, 1000, 16, sha256.New)
+}
diff --git a/router/dataplane.go b/router/dataplane.go
index 3e8466d3cb..233f2309e0 100644
--- a/router/dataplane.go
+++ b/router/dataplane.go
@@ -18,6 +18,7 @@ package router
import (
"context"
+ "crypto/cipher"
"crypto/subtle"
"encoding/binary"
"errors"
@@ -45,6 +46,7 @@ import (
"github.com/scionproto/scion/pkg/slayers/path"
"github.com/scionproto/scion/pkg/slayers/path/empty"
"github.com/scionproto/scion/pkg/slayers/path/epic"
+ hbird "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
"github.com/scionproto/scion/pkg/slayers/path/onehop"
"github.com/scionproto/scion/pkg/slayers/path/scion"
"github.com/scionproto/scion/pkg/spao"
@@ -229,6 +231,7 @@ type dataPlane struct {
neighborIAs [math.MaxUint16 + 1]addr.IA
localHost addr.Host
macFactory func() hash.Hash
+ prfFactory func() cipher.Block
localIA addr.IA
mtx sync.Mutex
running atomic.Bool
@@ -256,6 +259,9 @@ type dataPlane struct {
// link layer header. Underlay providers may use the preceding part of the packet buffer to
// receive the link layer header.
underlayHeadroom int
+
+ // Contains the token buckets for hummingbird bandwidth check.
+ tokenBuckets sync.Map
}
var (
@@ -976,7 +982,9 @@ func newPacketProcessor(d *dataPlane) *scionPacketProcessor {
p := &scionPacketProcessor{
d: d,
mac: d.macFactory(),
+ prf: d.prfFactory(),
macInputBuffer: make([]byte, max(path.MACBufferSize, libepic.MACBufferSize)),
+ hbirdXkbuffer: make([]uint32, hbird.XkBufferSize),
}
p.scionLayer.RecyclePaths()
return p
@@ -997,6 +1005,11 @@ func (p *scionPacketProcessor) reset() error {
p.hbhLayer = slayers.HopByHopExtnSkipper{}
// Reset e2e layer
p.e2eLayer = slayers.EndToEndExtnSkipper{}
+ // Hummingbird:
+ p.hbirdPath = nil
+ p.flyoverField = hbird.FlyoverHopField{}
+ p.hasPriority = false
+ p.isFlyoverXover = false
return nil
}
@@ -1044,6 +1057,8 @@ func (p *scionPacketProcessor) processPkt(pkt *Packet) disposition {
return p.processSCION()
case epic.PathType:
return p.processEPIC()
+ case hbird.PathType:
+ return p.processHummingbird()
default:
return errorDiscard("error", errUnsupportedPathType)
}
@@ -1139,6 +1154,13 @@ type scionPacketProcessor struct {
cachedMac []byte // Full MAC. For a Xover, that of the down segment.
macInputBuffer []byte // Reusable buffer for MAC computation.
bfdLayer layers.BFD // Reusable buffer for parsing BFD messages
+ // Hummingbird specific:
+ prf cipher.Block // Hummingbird authentication key derivation
+ hbirdPath *hbird.Raw // Raw Hummingbird path. Will be set during processing
+ flyoverField hbird.FlyoverHopField // Hummingbird flyover field
+ hasPriority bool // Determines Hummingbird priority
+ isFlyoverXover bool // True if this is a Hummingbird xover flyover
+ hbirdXkbuffer []uint32 // Reusable buffer for Hummingbird MAC
}
type slowPathType int8
@@ -1449,11 +1471,17 @@ func (p *scionPacketProcessor) updateNonConsDirIngressSegID() disposition {
}
func (p *scionPacketProcessor) currentInfoPointer() uint16 {
+ if p.scionLayer.PathType == hbird.PathType {
+ return p.currentHbirdInfoPointer()
+ }
return uint16(slayers.CmnHdrLen + p.scionLayer.AddrHdrLen() +
scion.MetaLen + path.InfoLen*int(p.path.PathMeta.CurrINF))
}
func (p *scionPacketProcessor) currentHopPointer() uint16 {
+ if p.scionLayer.PathType == hbird.PathType {
+ return p.currentHbirdHopPointer()
+ }
return uint16(slayers.CmnHdrLen + p.scionLayer.AddrHdrLen() +
scion.MetaLen + path.InfoLen*p.path.NumINF + path.HopLen*int(p.path.PathMeta.CurrHF))
}
@@ -2214,10 +2242,11 @@ func (p *slowPathPacketProcessor) prepareSCMP(
"path type", pathType)
}
path = epicPath.ScionPath
+ case hbird.PathType:
+ return p.prepareHbirdSCMP(typ, code, scmpP, isError)
default:
return serrors.JoinNoStack(errCannotRoute, nil, "details", "unsupported path type",
"path type", pathType)
-
}
decPath, err := path.ToDecoded()
if err != nil {
diff --git a/router/dataplane_hbird.go b/router/dataplane_hbird.go
new file mode 100644
index 0000000000..720b202166
--- /dev/null
+++ b/router/dataplane_hbird.go
@@ -0,0 +1,1038 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package router
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/subtle"
+ "fmt"
+ "time"
+
+ "github.com/gopacket/gopacket"
+
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/private/util"
+ "github.com/scionproto/scion/pkg/slayers"
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+ "github.com/scionproto/scion/pkg/spao"
+ "github.com/scionproto/scion/router/tokenbucket"
+)
+
+// SetHbirdKey sets the key for the PRF function used to compute the Hummingbird Auth Key.
+func (d *dataPlane) SetHbirdKey(key []byte) error {
+ d.mtx.Lock()
+ defer d.mtx.Unlock()
+ if d.isRunning() {
+ return errModifyExisting
+ }
+ if len(key) == 0 {
+ return errEmptyValue
+ }
+ if d.prfFactory != nil {
+ return errAlreadySet
+ }
+ // First check for cipher creation errors
+ if _, err := aes.NewCipher(key); err != nil {
+ return err
+ }
+ d.prfFactory = func() cipher.Block {
+ prf, _ := aes.NewCipher(key)
+ return prf
+ }
+ return nil
+}
+
+func (p *scionPacketProcessor) parseHbirdPath() disposition {
+ var err error
+ p.flyoverField, err = p.hbirdPath.GetCurrentHopField()
+ if err != nil {
+ return errorDiscard(err, errMalformedPath)
+ }
+ p.hopField = p.flyoverField.HopField
+ p.infoField, err = p.hbirdPath.GetCurrentInfoField()
+ if err != nil {
+ // TODO(lukedirtwalker) parameter problem invalid path?
+ return errorDiscard("error", err)
+ }
+ if p.flyoverField.Flyover {
+ p.hasPriority = true
+ }
+
+ return pForward
+}
+
+func determinePeerHbird(pathMeta hummingbird.MetaHdr, inf path.InfoField) (bool, error) {
+ if !inf.Peer {
+ return false, nil
+ }
+
+ if pathMeta.SegLen[0] == 0 {
+ return false, errPeeringEmptySeg0
+ }
+ if pathMeta.SegLen[1] == 0 {
+ return false, errPeeringEmptySeg1
+
+ }
+ if pathMeta.SegLen[2] != 0 {
+ return false, errPeeringNonemptySeg2
+ }
+
+ // The peer hop fields are the last hop field on the first path
+ // segment (at SegLen[0] - 1) and the first hop field of the second
+ // path segment (at SegLen[0]). The below check applies only
+ // because we already know this is a well-formed peering path.
+ currHF := pathMeta.CurrHF
+ segLen := pathMeta.SegLen[0]
+ peer := currHF == segLen-hummingbird.HopLines || currHF == segLen-hummingbird.FlyoverLines ||
+ currHF == segLen
+ return peer, nil
+}
+
+func (p *scionPacketProcessor) determinePeerHbird() disposition {
+ peer, err := determinePeerHbird(p.hbirdPath.PathMeta, p.infoField)
+ p.peering = peer
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ return pForward
+}
+
+func (p *scionPacketProcessor) validateHopExpiryHbird() disposition {
+ expiration := util.SecsToTime(p.infoField.Timestamp).
+ Add(path.ExpTimeToDuration(p.hopField.ExpTime))
+ expired := expiration.Before(time.Now())
+ if !expired {
+ return pForward
+ }
+ log.Debug("SCMP response", "cause", errExpiredHop,
+ "cons_dir", p.infoField.ConsDir, "if_id", p.ingressFromLink,
+ "curr_inf", p.hbirdPath.PathMeta.CurrINF, "curr_hf", p.hbirdPath.PathMeta.CurrHF)
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathType(slayers.SCMPTypeParameterProblem),
+ code: slayers.SCMPCodePathExpired,
+ pointer: p.currentHopPointer(),
+ }
+ return pSlowPath
+}
+
+func (p *scionPacketProcessor) validateReservationExpiry() disposition {
+ startTime := util.SecsToTime(p.hbirdPath.PathMeta.BaseTS - uint32(p.flyoverField.ResStartTime))
+ endTime := startTime.Add(time.Duration(p.flyoverField.Duration) * time.Second)
+ now := time.Now()
+ if startTime.Before(now) && now.Before(endTime) {
+ return pForward
+ }
+ log.Debug("SCMP: Reservation is not valid at current time", "reservation start", startTime,
+ "reservation end", endTime, "now", now)
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathType(slayers.SCMPTypeParameterProblem),
+ code: slayers.SCMPCodeReservationExpired,
+ pointer: p.currentHopPointer(),
+ }
+ return pSlowPath
+}
+
+func (p *scionPacketProcessor) currentHbirdInfoPointer() uint16 {
+ return uint16(slayers.CmnHdrLen + p.scionLayer.AddrHdrLen() +
+ hummingbird.MetaLen + path.InfoLen*int(p.hbirdPath.PathMeta.CurrINF))
+}
+
+func (p *scionPacketProcessor) currentHbirdHopPointer() uint16 {
+ return uint16(slayers.CmnHdrLen + p.scionLayer.AddrHdrLen() +
+ hummingbird.MetaLen + path.InfoLen*p.hbirdPath.NumINF +
+ hummingbird.LineLen*int(p.hbirdPath.PathMeta.CurrHF))
+}
+
+// Returns the ingress and egress through which the current packet enters and leves the AS
+func (p *scionPacketProcessor) getFlyoverInterfaces() (uint16, uint16, disposition) {
+ ingress := p.hopField.ConsIngress
+ egress := p.hopField.ConsEgress
+ // Reservations are not bidirectional,
+ // reservation ingress and egress are always real ingress and egress
+ if !p.infoField.ConsDir {
+ ingress, egress = egress, ingress
+ }
+ // On crossovers, A Reservation goes from the ingress of the incoming hop to
+ // the egress of the outgoing one
+ var err error
+ if p.hbirdPath.IsXover() && !p.peering {
+ egress, err = p.hbirdPath.GetNextEgress()
+ if err != nil {
+ return 0, 0, errorDiscard("error", err)
+ }
+ } else if p.hbirdPath.IsFirstHopAfterXover() && !p.peering {
+ ingress, err = p.hbirdPath.GetPreviousIngress()
+ if err != nil {
+ return 0, 0, errorDiscard("error", err)
+ }
+ }
+ return ingress, egress, pForward
+}
+
+func (p *scionPacketProcessor) verifyHbirdScionMac() disposition {
+ scionMac := path.FullMAC(p.mac, p.infoField, p.hopField, p.macInputBuffer[:path.MACBufferSize])
+ verified := subtle.ConstantTimeCompare(p.hopField.Mac[:path.MacLen], scionMac[:path.MacLen])
+ if verified == 0 {
+ log.Debug("SCMP: MAC verification failed", "expected", fmt.Sprintf(
+ "%x", scionMac[:path.MacLen]),
+ "actual", fmt.Sprintf("%x", p.hopField.Mac[:path.MacLen]),
+ "cons_dir", p.infoField.ConsDir,
+ "if_id", p.ingressFromLink, "curr_inf", p.hbirdPath.PathMeta.CurrINF,
+ "curr_hf", p.hbirdPath.PathMeta.CurrHF, "seg_id", p.infoField.SegID)
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathType(slayers.SCMPTypeParameterProblem),
+ code: slayers.SCMPCodeInvalidHopFieldMAC,
+ pointer: p.currentHopPointer(),
+ }
+ return pSlowPath
+ }
+ return pForward
+}
+
+func (p *scionPacketProcessor) verifyHbirdFlyoverMac() disposition {
+ var flyoverMac []byte
+ var verified int
+
+ ingress, egress, disp := p.getFlyoverInterfaces()
+ if disp != pForward {
+ return disp
+ }
+
+ ak := hummingbird.DeriveAuthKey(p.prf, p.flyoverField.ResID, p.flyoverField.Bw,
+ ingress, egress, p.hbirdPath.PathMeta.BaseTS-uint32(p.flyoverField.ResStartTime),
+ p.flyoverField.Duration,
+ p.macInputBuffer[path.MACBufferSize+hummingbird.FlyoverMacBufferSize:])
+ flyoverMac = hummingbird.FullFlyoverMac(ak, p.scionLayer.DstIA, p.scionLayer.PayloadLen,
+ p.flyoverField.ResStartTime, p.hbirdPath.PathMeta.HighResTS,
+ p.macInputBuffer[path.MACBufferSize:], p.hbirdXkbuffer)
+
+ if !p.hbirdPath.IsFirstHopAfterXover() {
+ disp := p.updateHbirdNonConsDirIngressSegIDFlyover(flyoverMac)
+ if disp != pForward {
+ return disp
+ }
+ }
+ scionMac := path.FullMAC(p.mac, p.infoField, p.hopField, p.macInputBuffer[:path.MACBufferSize])
+
+ macXor(flyoverMac[:], scionMac[:], flyoverMac[:])
+ verified = subtle.ConstantTimeCompare(p.hopField.Mac[:path.MacLen], flyoverMac[:path.MacLen])
+ if verified == 0 {
+ log.Debug("SCMP: Aggregate MAC verification failed",
+ "expected", fmt.Sprintf("%x", flyoverMac[:path.MacLen]),
+ "actual", fmt.Sprintf("%x", p.hopField.Mac[:path.MacLen]),
+ "cons_dir", p.infoField.ConsDir,
+ "scionMac", fmt.Sprintf("%x", scionMac[:path.MacLen]),
+ "if_id", p.ingressFromLink, "curr_inf", p.hbirdPath.PathMeta.CurrINF,
+ "curr_hf", p.hbirdPath.PathMeta.CurrHF, "seg_id", p.infoField.SegID,
+ "packet length", p.scionLayer.PayloadLen,
+ "dest", p.scionLayer.DstIA, "startTime", p.flyoverField.ResStartTime,
+ "highResTS", p.hbirdPath.PathMeta.HighResTS,
+ "ResID", p.flyoverField.ResID, "Bw", p.flyoverField.Bw,
+ "in", p.hopField.ConsIngress, "Eg", p.hopField.ConsEgress,
+ "start ak", p.hbirdPath.PathMeta.BaseTS-uint32(p.flyoverField.ResStartTime),
+ "Duration", p.flyoverField.Duration)
+ }
+
+ // Add the full MAC to the SCION packet processor,
+ // such that hummingbird mac de-aggregation do not need to recalculate it.
+ // Do not overwrite cachedmac after doing xover, as it may contain a flyovermac
+ // This function is currently not called after a xover, so no need to check
+ // Keep in mind for future changes
+ p.cachedMac = scionMac
+
+ if verified == 0 {
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathType(slayers.SCMPTypeParameterProblem),
+ code: slayers.SCMPCodeInvalidHopFieldMAC,
+ pointer: p.currentHopPointer(),
+ }
+ return pSlowPath
+ }
+ return pForward
+}
+
+func (p *scionPacketProcessor) validateHbirdSrcDstIA() disposition {
+ srcIsLocal := (p.scionLayer.SrcIA == p.d.localIA)
+ dstIsLocal := (p.scionLayer.DstIA == p.d.localIA)
+ if p.ingressFromLink == 0 {
+ // Outbound
+ // Only check SrcIA if first hop, for transit this already checked by ingress router.
+ // Note: SCMP error messages triggered by the sibling router may use paths that
+ // don't start with the first hop.
+ if p.hbirdPath.IsFirstHop() && !srcIsLocal {
+ return p.respInvalidSrcIA()
+ }
+ if dstIsLocal {
+ return p.respInvalidSrcIA()
+ }
+ } else {
+ // Inbound
+ if srcIsLocal {
+ return p.respInvalidSrcIA()
+ }
+ if p.hbirdPath.IsLastHop() != dstIsLocal {
+ return p.respInvalidDstIA()
+ }
+ }
+ return pForward
+}
+
+func (p *scionPacketProcessor) ingressInterfaceHbird() uint16 {
+ info := p.infoField
+ hop := p.flyoverField
+ if !p.peering && p.hbirdPath.IsFirstHopAfterXover() {
+ var err error
+ info, err = p.hbirdPath.GetInfoField(int(p.hbirdPath.PathMeta.CurrINF) - 1)
+ if err != nil { // cannot be out of range
+ panic(err)
+ }
+ // Previous hop should always be a non-flyover field,
+ // as flyover is transferred to second hop on xover
+ hop, err = p.hbirdPath.GetHopField(int(p.hbirdPath.PathMeta.CurrHF) - hummingbird.HopLines)
+ if err != nil { // cannot be out of range
+ panic(err)
+ }
+ }
+ if info.ConsDir {
+ return hop.HopField.ConsIngress
+ }
+ return hop.HopField.ConsEgress
+}
+
+// validateTransitUnderlaySrc checks that the source address of transit packets
+// matches the expected sibling router.
+// Provided that underlying network infrastructure prevents address spoofing,
+// this check prevents malicious end hosts in the local AS from bypassing the
+// SrcIA checks by disguising packets as transit traffic.
+func (p *scionPacketProcessor) validateHbirdTransitUnderlaySrc() disposition {
+ if p.hbirdPath.IsFirstHop() || p.ingressFromLink != 0 {
+ // Locally originated traffic, or came in via an external link. Not our concern.
+ return pForward
+ }
+ pktIngressID := p.ingressInterfaceHbird() // Where this was *supposed* to enter the AS
+ ingressLink := p.d.interfaces[pktIngressID] // Our own link to *that* sibling router
+
+ // Is that the link that the packet came through (e.g. not the internal link)? The
+ // comparison should be cheap. Links are implemented by pointers.
+ if ingressLink != p.pkt.Link {
+ // Drop
+ return errorDiscard("error", errInvalidSrcAddrForTransit)
+ }
+ return pForward
+}
+
+// Verifies the PathMetaHeader timestamp is recent
+// Current implementation works with a nanosecond granularity HighResTS
+func (p *scionPacketProcessor) validatePathMetaTimestamp() {
+ timestamp := util.SecsToTime(p.hbirdPath.PathMeta.BaseTS).Add(
+ time.Duration(p.hbirdPath.PathMeta.HighResTS>>22) * time.Millisecond)
+ // TODO: make a configurable value instead of using a flat 1 seconds
+ if time.Until(timestamp).Abs() > time.Duration(1)*time.Second {
+ // Hummingbird specification explicitly says to forward best-effort is timestamp too old
+ p.hasPriority = false
+ }
+}
+
+// Converts a flyover bandwidth value to bytes per second
+func convertResBw(bw uint16) float64 {
+
+ // In this implementation, we choose to allow reservations up to 64 kBps
+ // Since the bandwidth field has 10 bits, we multiply by 64 to reach the target range
+ return float64(bw * 64)
+}
+
+func (p *scionPacketProcessor) checkReservationBandwidth() disposition {
+ // Only check bandwidth if packet is given priority
+ // Bandwidth check is NOT performed for late packets that have flyover but no priority
+ if !p.hasPriority {
+ return pForward
+ }
+ // resID only has to be unique per interface pair
+ // key for the tokenbuckets map is based on flyover resID, ingress and egress
+ ingress, egress, disp := p.getFlyoverInterfaces()
+ if disp != pForward {
+ return disp
+ }
+
+ // Get the token bucket or add a new one.
+ resKey := uint64(p.flyoverField.ResID) + uint64(ingress)<<22 + uint64(egress)<<38
+ resBw := convertResBw(p.flyoverField.Bw)
+ now := time.Now()
+ v, _ := p.d.tokenBuckets.LoadOrStore(
+ resKey,
+ tokenbucket.NewTokenBucket(now, resBw, resBw))
+ tb, ok := v.(*tokenbucket.TokenBucket)
+ if !ok {
+ log.Error("Non-tokenbucket value found in tokenbucket map")
+ // This is an internal error that should never happen. We can't verify the BW.
+ return pForward
+ }
+
+ // Check bandwidth
+ if tb.CIR != resBw {
+ // It is possible for different reservations to share a resID
+ // if they do not overlap in time.
+ tb.SetRate(resBw)
+ tb.SetBurstSize(resBw)
+ }
+ if tb.Apply(int(p.scionLayer.PayloadLen), time.Now()) {
+ return pForward
+ }
+ // TODO: introduce priorities
+ return pForward
+
+}
+
+func (p *scionPacketProcessor) handleHbirdIngressRouterAlert() disposition {
+ if p.ingressFromLink == 0 {
+ return pForward
+ }
+ alert := p.ingressRouterAlertFlag()
+ if !*alert {
+ return pForward
+ }
+ // We have an alert.
+ *alert = false
+ err := p.hbirdPath.SetHopField(p.flyoverField, int(p.hbirdPath.PathMeta.CurrHF))
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathRouterAlertIngress,
+ }
+ return pSlowPath
+}
+
+func (p *scionPacketProcessor) handleHbirdEgressRouterAlert() disposition {
+ alert := p.egressRouterAlertFlag()
+ if !*alert {
+ return pForward
+ }
+ if p.d.interfaces[p.pkt.egress].Scope() != External {
+ // the egress router is not this one.
+ return pForward
+ }
+ *alert = false
+ err := p.hbirdPath.SetHopField(p.flyoverField, int(p.hbirdPath.PathMeta.CurrHF))
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ p.pkt.slowPathRequest = slowPathRequest{
+ spType: slowPathRouterAlertEgress,
+ }
+ return pSlowPath
+}
+
+func (p *scionPacketProcessor) updateHbirdNonConsDirIngressSegIDFlyover(flyoverMac []byte) disposition {
+ // against construction dir the ingress router updates the SegID, ifID == 0
+ // means this comes from this AS itself, so nothing has to be done.
+ // If a flyover is present, need to first de-aggregate the first two bytes of the mac
+ // before updating SegID
+ if !p.infoField.ConsDir && p.ingressFromLink != 0 && !p.peering {
+ // de-aggregate first two bytes of mac
+ p.hopField.Mac[0] ^= flyoverMac[0]
+ p.hopField.Mac[1] ^= flyoverMac[1]
+ p.infoField.UpdateSegID(p.hopField.Mac)
+ // restore correct state of MAC field, even if error
+ p.hopField.Mac[0] ^= flyoverMac[0]
+ p.hopField.Mac[1] ^= flyoverMac[1]
+ err := p.hbirdPath.SetInfoField(p.infoField, int(p.hbirdPath.PathMeta.CurrINF))
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ }
+ return pForward
+}
+
+func (p *scionPacketProcessor) updateHbirdNonConsDirIngressSegID() disposition {
+ // against construction dir the ingress router updates the SegID, ifID == 0
+ // means this comes from this AS itself, so nothing has to be done.
+ if !p.infoField.ConsDir && p.ingressFromLink != 0 && !p.peering {
+ p.infoField.UpdateSegID(p.hopField.Mac)
+ err := p.hbirdPath.SetInfoField(p.infoField, int(p.hbirdPath.PathMeta.CurrINF))
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ }
+ return pForward
+}
+
+// macXor XORs a and b and writes the result into d.
+// Expects all arguments to have a length of macLen
+func macXor(d, a, b []byte) {
+ for i := 0; i < path.MacLen; i++ {
+ d[i] = a[i] ^ b[i]
+ }
+}
+
+func (p *scionPacketProcessor) deAggregateMac() disposition {
+ if !p.flyoverField.Flyover {
+ return pForward
+ }
+ copy(p.hopField.Mac[:], p.cachedMac[:path.MacLen])
+ if err := p.hbirdPath.ReplaceCurrentMac(p.cachedMac); err != nil {
+ log.Debug("Failed to replace MAC after de-aggregation", "error", err.Error())
+ return errorDiscard("error", fmt.Errorf("MAC replacement failed"))
+ }
+ return pForward
+}
+
+// de-aggregates mac and stores the flyovermac part of the mac in cachedMac
+func (p *scionPacketProcessor) deAggregateAndCacheMac() disposition {
+ if !p.flyoverField.Flyover {
+ return pForward
+ }
+ // obtain flyoverMac and buffer in macInputBuffer
+ // such that it is not overwritten by the following standard mac computation
+ macXor(p.macInputBuffer[path.MACBufferSize:], p.cachedMac, p.hopField.Mac[:])
+ // deaggregate Mac
+ copy(p.hopField.Mac[:], p.cachedMac[:path.MacLen])
+ if err := p.hbirdPath.ReplaceCurrentMac(p.cachedMac); err != nil {
+ log.Debug("Failed to replace MAC after de-aggregation", "error", err.Error())
+ return errorDiscard("error", fmt.Errorf("MAC replacement failed"))
+ }
+ // set cachedMac to the buffered flyoverMac
+ p.cachedMac = p.macInputBuffer[path.MACBufferSize : path.MACBufferSize+path.MacLen]
+ return pForward
+}
+
+// xoverMoveFlyoverToNext is called during ASTransit incoming BR. It moves the flyover hopfield
+// to the next hopfield, so that the ASTransit outgoing BR forwards it with priority.
+func (p *scionPacketProcessor) xoverMoveFlyoverToNext() disposition {
+ // Move flyoverhopfield to next hop for benefit of egress router
+ if err := p.hbirdPath.MoveFlyoverToNext(); err != nil {
+ return errorDiscard("error", err)
+ }
+
+ // Aggregate mac of current hopfield with buffered flyoverMac
+ mac, err := p.hbirdPath.GetMac(int(p.hbirdPath.PathMeta.CurrHF))
+ if err != nil {
+ return errorDiscard("error", err)
+ }
+ macXor(mac, mac, p.cachedMac)
+ return pForward
+}
+
+func (p *scionPacketProcessor) xoverMoveFlyoverToPrevious() disposition {
+ if err := p.hbirdPath.MoveFlyoverToPrevious(); err != nil {
+ return errorDiscard("error", err)
+ }
+ // No MAC aggregation/de-aggregation, as these are already performed
+ p.flyoverField.Flyover = false
+ return pForward
+}
+
+func (p *scionPacketProcessor) doHbirdXoverFlyover() disposition {
+ p.effectiveXover = true
+ p.isFlyoverXover = true
+
+ if disp := p.deAggregateAndCacheMac(); disp != pForward {
+ return disp
+ }
+
+ if err := p.hbirdPath.IncPath(hummingbird.FlyoverLines); err != nil {
+ return errorDiscard("error", err)
+ }
+
+ var err error
+ if p.flyoverField, err = p.hbirdPath.GetCurrentHopField(); err != nil {
+ return errorDiscard("error", err)
+ }
+ if p.infoField, err = p.hbirdPath.GetCurrentInfoField(); err != nil {
+ return errorDiscard("error", err)
+ }
+ p.hopField = p.flyoverField.HopField
+ return pForward
+}
+
+func (p *scionPacketProcessor) doHbirdXoverBestEffort() disposition {
+ p.effectiveXover = true
+
+ if err := p.hbirdPath.IncPath(hummingbird.HopLines); err != nil {
+ // TODO parameter problem invalid path
+ return errorDiscard("error", err)
+ }
+
+ var err error
+ if p.flyoverField, err = p.hbirdPath.GetCurrentHopField(); err != nil {
+ // TODO parameter problem invalid path
+ return errorDiscard("error", err)
+ }
+ if p.infoField, err = p.hbirdPath.GetCurrentInfoField(); err != nil {
+ // TODO parameter problem invalid path
+ return errorDiscard("error", err)
+ }
+ p.hopField = p.flyoverField.HopField
+ return pForward
+}
+
+func (p *scionPacketProcessor) processHbirdEgress() disposition {
+ // We are the egress router and if we go in construction direction we
+ // need to update the SegID (unless we are effecting a peering hop).
+ // When we're at a peering hop, the SegID for this hop and for the next
+ // are one and the same, both hops chain to the same parent. So do not
+ // update SegID.
+ if p.infoField.ConsDir && !p.peering {
+ p.infoField.UpdateSegID(p.hopField.Mac)
+ if err := p.hbirdPath.SetInfoField(
+ p.infoField, int(p.hbirdPath.PathMeta.CurrINF)); err != nil {
+ // TODO parameter problem invalid path
+ return errorDiscard("error", err)
+ }
+ }
+ n := hummingbird.HopLines
+ if p.flyoverField.Flyover {
+ n = hummingbird.FlyoverLines
+ }
+ if err := p.hbirdPath.IncPath(n); err != nil {
+ // TODO parameter problem invalid path
+ return errorDiscard("error", err)
+ }
+ return pForward
+}
+
+// func (p *scionPacketProcessor) processHummingbird() (processResult, error) {
+func (p *scionPacketProcessor) processHummingbird() disposition {
+ var ok bool
+ p.hbirdPath, ok = p.scionLayer.Path.(*hummingbird.Raw)
+ if !ok {
+ // TODO(lukedirtwalker) parameter problem invalid path?
+ return errorDiscard("error", errMalformedPath)
+ }
+ if disp := p.parseHbirdPath(); disp != pForward {
+ return disp
+ }
+ if disp := p.determinePeerHbird(); disp != pForward {
+ return disp
+ }
+ // deleteme uncomment
+ if disp := p.validateHopExpiryHbird(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateIngressID(); disp != pForward {
+ return disp
+ }
+ if disp := p.validatePktLen(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateHbirdTransitUnderlaySrc(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateHbirdSrcDstIA(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateSrcHost(); disp != pForward {
+ return disp
+ }
+ if p.flyoverField.Flyover {
+ return p.processHBIRDFlyover()
+ }
+ return p.processHBIRDBestEffort()
+}
+
+func (p *scionPacketProcessor) processHBIRDFlyover() disposition {
+ // deleteme uncomment
+ if disp := p.validateReservationExpiry(); disp != pForward {
+ return disp
+ }
+ if disp := p.verifyHbirdFlyoverMac(); disp != pForward {
+ return disp
+ }
+ p.validatePathMetaTimestamp()
+ if disp := p.checkReservationBandwidth(); disp != pForward {
+ return disp
+ }
+ if disp := p.handleHbirdIngressRouterAlert(); disp != pForward {
+ return disp
+ }
+ // Inbound: pkts destined to the local IA.
+ if p.scionLayer.DstIA == p.d.localIA {
+ if disp := p.deAggregateMac(); disp != pForward {
+ return disp
+ }
+ disp := p.resolveInbound()
+ if disp != pForward {
+ return disp
+ }
+ p.pkt.trafficType = ttIn
+ return pForward
+ }
+
+ // Outbound: pkt leaving the local IA. This Could be:
+ // * Pure outbound: from this AS, in via internal, out via external.
+ // * ASTransit in: from another AS, in via external, out via internal to other BR.
+ // * ASTransit out: from another AS, in via internal from other BR, out via external.
+ // * BRTransit: from another AS, in via external, out via external.
+ if p.hbirdPath.IsXover() && !p.peering {
+ // An effective cross-over is a change of segment other than at
+ // a peering hop.
+ if disp := p.doHbirdXoverFlyover(); disp != pForward {
+ return disp
+ }
+ // doXover() has changed the current segment and hop field.
+ // We need to validate the new hop field.
+ if disp := p.validateHopExpiry(); disp != pForward {
+ return disp
+ }
+ // verify the new hopField
+ if disp := p.verifyHbirdScionMac(); disp != pForward {
+ return disp
+ }
+ }
+ egressID := p.egressInterface()
+ p.pkt.egress = egressID
+ if disp := p.validateEgressID(); disp != pForward {
+ return disp
+ }
+
+ // handle egress router alert before we check if it's up because we want to
+ // send the reply anyway, so that trace route can pinpoint the exact link
+ // that failed.
+ if disp := p.handleHbirdEgressRouterAlert(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateEgressUp(); disp != pForward {
+ return disp
+ }
+
+ if p.d.interfaces[egressID].Scope() == External {
+ // Not ASTransit in.
+ if disp := p.deAggregateMac(); disp != pForward {
+ return disp
+ }
+ if p.hbirdPath.IsFirstHopAfterXover() && !p.effectiveXover && !p.peering {
+ if disp := p.xoverMoveFlyoverToPrevious(); disp != pForward {
+ return disp
+ }
+ }
+ if disp := p.processHbirdEgress(); disp != pForward {
+ return disp
+ }
+ // Finish deciding the trafficType...
+ var tt trafficType
+ if p.scionLayer.SrcIA == p.d.localIA {
+ // Pure outbound
+ tt = ttOut
+ } else if p.ingressFromLink == 0 {
+ // ASTransit out
+ tt = ttOutTransit
+ } else {
+ // Therefore it is BRTransit
+ tt = ttBrTransit
+ }
+ p.pkt.trafficType = tt
+ return pForward
+ }
+ // ASTransit in: pkt leaving this AS through another BR.
+ // We already know the egressID is valid. The packet can go straight to forwarding.
+ if p.isFlyoverXover {
+ if disp := p.xoverMoveFlyoverToNext(); disp != pForward {
+ return disp
+ }
+ }
+ p.pkt.trafficType = ttInTransit
+ return pForward
+}
+
+func (p *scionPacketProcessor) processHBIRDBestEffort() disposition {
+ if disp := p.updateHbirdNonConsDirIngressSegID(); disp != pForward {
+ return disp
+ }
+ if disp := p.verifyHbirdScionMac(); disp != pForward {
+ return disp
+ }
+ if disp := p.handleHbirdIngressRouterAlert(); disp != pForward {
+ return disp
+ }
+ // Inbound: pkts destined to the local IA.
+ if p.scionLayer.DstIA == p.d.localIA {
+ disp := p.resolveInbound()
+ if disp != pForward {
+ return disp
+ }
+ p.pkt.trafficType = ttIn
+ return pForward
+ }
+
+ // Outbound: pkt leaving the local IA. This Could be:
+ // * Pure outbound: from this AS, in via internal, out via external.
+ // * ASTransit in: from another AS, in via external, out via internal to other BR.
+ // * ASTransit out: from another AS, in via internal from other BR, out via external.
+ // * BRTransit: from another AS, in via external, out via external.
+ if p.hbirdPath.IsXover() && !p.peering {
+ if disp := p.doHbirdXoverBestEffort(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateHopExpiryHbird(); disp != pForward {
+ return disp
+ }
+ // verify the new hopField
+ if disp := p.verifyHbirdScionMac(); disp != pForward {
+ return disp
+ }
+ }
+ egressID := p.egressInterface()
+ p.pkt.egress = egressID
+ if disp := p.validateEgressID(); disp != pForward {
+ return disp
+ }
+
+ // handle egress router alert before we check if it's up because we want to
+ // send the reply anyway, so that trace route can pinpoint the exact link
+ // that failed.
+ if disp := p.handleHbirdEgressRouterAlert(); disp != pForward {
+ return disp
+ }
+ if disp := p.validateEgressUp(); disp != pForward {
+ return disp
+ }
+
+ if p.d.interfaces[egressID].Scope() == External {
+ if disp := p.processHbirdEgress(); disp != pForward {
+ return disp
+ }
+ // Finish deciding the trafficType...
+ var tt trafficType
+ if p.scionLayer.SrcIA == p.d.localIA {
+ // Pure outbound
+ tt = ttOut
+ } else if p.ingressFromLink == 0 {
+ // ASTransit out
+ tt = ttOutTransit
+ } else {
+ // Therefore it is BRTransit
+ tt = ttBrTransit
+ }
+ p.pkt.trafficType = tt
+ return pForward
+ }
+
+ // ASTransit in: pkt leaving this AS through another BR.
+ // We already know the egressID is valid. The packet can go straight to forwarding.
+ p.pkt.trafficType = ttInTransit
+ return pForward
+}
+
+// Functions for SCMP packets preparation
+
+func (p *slowPathPacketProcessor) prepareHbirdSCMP(
+ typ slayers.SCMPType,
+ code slayers.SCMPCode,
+ scmpP gopacket.SerializableLayer,
+ isError bool,
+) error {
+ path, ok := p.scionLayer.Path.(*hummingbird.Raw)
+ if !ok {
+ return serrors.JoinNoStack(errCannotRoute, nil, "details", "unsupported path type",
+ "path type", p.scionLayer.Path.Type())
+ }
+
+ decPath, err := path.ToDecoded()
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "decoding raw path")
+ }
+ revPathTmp, err := decPath.Reverse()
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "reversing path for SCMP")
+ }
+ revPath := revPathTmp.(*hummingbird.Decoded)
+
+ peering, err := determinePeerHbird(revPath.PathMeta, revPath.InfoFields[revPath.PathMeta.CurrINF])
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "peering cannot be determined")
+ }
+
+ // Revert potential path segment switches that were done during processing.
+ if revPath.IsXover() && !peering {
+ // An effective cross-over is a change of segment other than at
+ // a peering hop.
+ if err := revPath.IncPath(hummingbird.HopLines); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err,
+ "details", "reverting cross over for SCMP")
+ }
+ }
+ // If the packet is sent to an external router, we need to increment the
+ // path to prepare it for the next hop.
+ // This is an SCMP response to pkt, so the egress link will be the ingress link.
+ if p.pkt.Link.Scope() == External {
+ infoField := &revPath.InfoFields[revPath.PathMeta.CurrINF]
+ if infoField.ConsDir && !peering {
+ hopField := revPath.HopFields[revPath.PathMeta.CurrHF]
+ infoField.UpdateSegID(hopField.HopField.Mac)
+ }
+ if err := revPath.IncPath(hummingbird.HopLines); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err,
+ "details", "incrementing path for SCMP")
+ }
+ }
+
+ // create new SCION header for reply.
+ var scionL slayers.SCION
+ scionL.FlowID = p.scionLayer.FlowID
+ scionL.TrafficClass = p.scionLayer.TrafficClass
+ scionL.PathType = revPath.Type()
+ scionL.Path = revPath
+ scionL.DstIA = p.scionLayer.SrcIA
+ scionL.SrcIA = p.d.localIA
+ scionL.DstAddrType = p.scionLayer.SrcAddrType
+ scionL.RawDstAddr = p.scionLayer.RawSrcAddr
+ scionL.NextHdr = slayers.L4SCMP
+
+ if err := scionL.SetSrcAddr(p.d.localHost); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "setting src addr")
+ }
+ typeCode := slayers.CreateSCMPTypeCode(typ, code)
+ scmpH := slayers.SCMP{TypeCode: typeCode}
+ scmpH.SetNetworkLayerForChecksum(&scionL)
+
+ needsAuth := false
+ if p.d.ExperimentalSCMPAuthentication {
+ // Error messages must be authenticated.
+ // Traceroute are OPTIONALLY authenticated ONLY IF the request
+ // was authenticated.
+ // TODO(JordiSubira): Reuse the key computed in p.hasValidAuth
+ // if SCMPTypeTracerouteReply to create the response.
+ needsAuth = isError ||
+ (scmpH.TypeCode.Type() == slayers.SCMPTypeTracerouteReply &&
+ p.hasValidAuth(time.Now()))
+ }
+
+ sopts := gopacket.SerializeOptions{
+ ComputeChecksums: true,
+ FixLengths: true,
+ }
+ var serBuf serializeProxy
+
+ // First write the SCMP message only without the SCION header(s) to get a buffer that we can
+ // feed to the MAC computation. If this is an error response, then it has to include a quote of
+ // the packet at the end of the SCMP message.
+
+ if isError {
+ // There is headroom built into the packet buffer so we can wrap the whole packet into a new
+ // one without copying it. We need to reclaim that headroom so we can prepend. We can figure
+ // the current headroom, even if it was changed, by comparing the capacity of the slice with
+ // our constant buffer size.
+ quoteLen := len(p.pkt.RawPacket)
+ headroom := len(p.pkt.buffer) - cap(p.pkt.RawPacket)
+ hdrLen := slayers.CmnHdrLen + scionL.AddrHdrLen() + scionL.Path.Len() +
+ slayers.ScmpHeaderSize(scmpH.TypeCode.Type())
+
+ if needsAuth {
+ hdrLen += e2eAuthHdrLen
+ }
+ maxQuoteLen := slayers.MaxSCMPPacketLen - hdrLen
+ if quoteLen > maxQuoteLen {
+ quoteLen = maxQuoteLen
+ }
+ // Now that we know the length, we can serialize the SCMP headers and the quoted packet. If
+ // we don't fit in the headroom we copy the quoted packet to the end. We are required to
+ // leave space for a worst-case underlay header too. TODO(multi_underlay): since we know
+ // that this goes back via the link it came from, we could be content with leaving just
+ // enough headroom for this specific underlay.
+ if hdrLen+p.d.underlayHeadroom > headroom {
+ // Not enough headroom. Pack at end.
+ quote := p.pkt.RawPacket[:quoteLen]
+ serBuf = newSerializeProxy(p.pkt.RawPacket)
+ err = gopacket.SerializeLayers(&serBuf, sopts, &scmpH, scmpP, gopacket.Payload(quote))
+ if err != nil {
+ return serrors.JoinNoStack(
+ errCannotRoute, err, "details", "serializing SCMP message")
+ }
+ } else {
+ // Serialize in front of the quoted packet. The quoted packet must be included in the
+ // serialize buffer before we pack the SCMP header in from of it. AppendBytes will do
+ // that; it exposes the underlying buffer but doesn't modify it.
+ p.pkt.RawPacket = p.pkt.buffer[0:(quoteLen + headroom)]
+ serBuf = newSerializeProxyStart(p.pkt.RawPacket, headroom)
+ _, _ = serBuf.AppendBytes(quoteLen) // Implementation never fails.
+ err = scmpP.SerializeTo(&serBuf, sopts)
+ if err != nil {
+ return serrors.JoinNoStack(
+ errCannotRoute, err, "details", "serializing SCMP message")
+ }
+ err = scmpH.SerializeTo(&serBuf, sopts)
+ if err != nil {
+ return serrors.JoinNoStack(
+ errCannotRoute, err, "details", "serializing SCMP message")
+ }
+ }
+ } else {
+ // We do not need to preserve the packet. Just pack our headers at the end of the buffer.
+ // (this is what serializeProxy does by default).
+ serBuf = newSerializeProxy(p.pkt.RawPacket)
+ err = gopacket.SerializeLayers(&serBuf, sopts, &scmpH, scmpP)
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "serializing SCMP message")
+ }
+ }
+
+ // serBuf now starts with the SCMP Headers and ends with the truncated quoted packet, if any.
+ // This is what gets checksumed.
+ if needsAuth {
+ var e2e slayers.EndToEndExtn
+ scionL.NextHdr = slayers.End2EndClass
+
+ now := time.Now()
+ dstA, err := scionL.DstAddr()
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err,
+ "details", "parsing destination address")
+ }
+ key, err := p.drkeyProvider.GetASHostKey(now, scionL.DstIA, dstA)
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "retrieving DRKey")
+ }
+ if err := p.resetSPAOMetadata(key, now); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "resetting SPAO header")
+ }
+
+ e2e.Options = []*slayers.EndToEndOption{p.optAuth.EndToEndOption}
+ e2e.NextHdr = slayers.L4SCMP
+ _, err = spao.ComputeAuthCMAC(
+ spao.MACInput{
+ Key: key.Key[:],
+ Header: p.optAuth,
+ ScionLayer: &scionL,
+ PldType: slayers.L4SCMP,
+ Pld: serBuf.Bytes(),
+ },
+ p.macInputBuffer,
+ p.optAuth.Authenticator(),
+ )
+ if err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "computing CMAC")
+ }
+ if err := e2e.SerializeTo(&serBuf, sopts); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err,
+ "details", "serializing SCION E2E headers")
+ }
+ } else {
+ scionL.NextHdr = slayers.L4SCMP
+ }
+
+ // Our SCION header is ready. Prepend it.
+ if err := scionL.SerializeTo(&serBuf, sopts); err != nil {
+ return serrors.JoinNoStack(errCannotRoute, err, "details", "serializing SCION header")
+ }
+
+ // serBuf now has the exact slice that represents the packet.
+ p.pkt.RawPacket = serBuf.Bytes()
+
+ log.Debug("SCMP", "typecode", scmpH.TypeCode)
+ return nil
+}
diff --git a/router/dataplane_hbird_test.go b/router/dataplane_hbird_test.go
new file mode 100644
index 0000000000..383d74fa2a
--- /dev/null
+++ b/router/dataplane_hbird_test.go
@@ -0,0 +1,3765 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package router_test
+
+import (
+ "crypto/aes"
+ "net"
+ "net/netip"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/util"
+ "github.com/scionproto/scion/pkg/slayers"
+ "github.com/scionproto/scion/pkg/slayers/path"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+ "github.com/scionproto/scion/private/topology"
+ "github.com/scionproto/scion/router"
+)
+
+func TestDataPlaneSetHbirdKey(t *testing.T) {
+ t.Run("fails after serve", func(t *testing.T) {
+ d := router.NewDPRaw(router.RunConfig{}, false)
+ d.MockStart()
+ assert.Error(t, d.SetHbirdKey([]byte("dummy")))
+ })
+ t.Run("setting nil value is not allowed", func(t *testing.T) {
+ d := router.NewDPRaw(router.RunConfig{}, false)
+ d.MockStart()
+ assert.Error(t, d.SetHbirdKey(nil))
+ })
+ t.Run("single set works", func(t *testing.T) {
+ d := router.NewDPRaw(router.RunConfig{}, false)
+ assert.NoError(t, d.SetHbirdKey([]byte("dummy key xxxxxx")))
+ })
+ t.Run("double set fails", func(t *testing.T) {
+ d := router.NewDPRaw(router.RunConfig{}, false)
+ assert.NoError(t, d.SetHbirdKey([]byte("dummy key xxxxxx")))
+ assert.Error(t, d.SetHbirdKey([]byte("dummy key xxxxxx")))
+ })
+}
+
+func TestProcessHbirdPacket(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ key := []byte("testkey_xxxxxxxx")
+ otherKey := []byte("testkey_yyyyyyyy")
+ hbirdKey := []byte("test_secretvalue")
+ now := time.Now()
+ // now := time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC) // deleteme
+
+ // ProcessPacket assumes some pre-conditions:
+ // * The ingress interface has to exist. This mock map is good for most test cases.
+ // Others need a custom one.
+ // * InternalNextHops may not be nil. Empty is ok (sufficient unless testing AS transit).
+ mockExternalInterfaces := []uint16{1, 2, 3}
+ mockInternalNextHops := map[uint16]netip.AddrPort{}
+
+ testCases := map[string]struct {
+ prepareDP func(*gomock.Controller) *router.DataPlane
+ mockMsg func(bool, *router.DataPlane) *router.Packet
+ assertFunc func(*testing.T, router.Disposition)
+ }{
+ "inbound": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ mockExternalInterfaces,
+ nil,
+ nil,
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ spkt.DstIA = addr.MustParseIA("1-ff00:0:110")
+ dst := addr.MustParseHost("10.0.100.100")
+ assert.NoError(t, spkt.SetDstAddr(dst))
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 41, ConsEgress: 40}},
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 01, ConsEgress: 0}},
+ }
+ dpath.Base.PathMeta.CurrHF = 6
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[2].HopField)
+ var dstAddr *net.UDPAddr
+ ingress := uint16(1)
+ egress := uint16(0)
+ if afterProcessing {
+ dstAddr = &net.UDPAddr{IP: dst.IP().AsSlice(), Port: dstUDPPort}
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "outbound": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1},
+ map[uint16]topology.LinkType{
+ 1: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ spkt.SrcIA = addr.MustParseIA("1-ff00:0:110")
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1}},
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 41, ConsEgress: 40}},
+ }
+ dpath.Base.PathMeta.CurrHF = 0
+ dpath.HopFields[0].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[0].HopField)
+ ingress := uint16(0)
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+ egress = 1
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Parent,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ }
+ dpath.Base.PathMeta.CurrHF = 3
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ ingress := uint16(1)
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit non consdir": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 2: topology.Parent,
+ 1: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 2, ConsEgress: 1}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ }
+ dpath.Base.PathMeta.CurrHF = 3
+ dpath.InfoFields[0].ConsDir = false
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ ingress := uint16(1)
+ egress := uint16(0)
+ if afterProcessing {
+ require.NoError(t, dpath.IncPath(hummingbird.HopLines))
+ egress = 2
+ } else {
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit peering consdir": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet just left segment 0 which ends at
+ // (peering) hop 0 and is landing on segment 1 which
+ // begins at (peering) hop 1. We do not care what hop 0
+ // looks like. The forwarding code is looking at hop 1 and
+ // should leave the message in shape to be processed at hop 2.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 1,
+ SegLen: [3]uint8{3, 6, 0},
+ },
+ NumINF: 2,
+ NumLines: 12,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ // core seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ },
+ }
+
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. However, the forwarding code isn't
+ // supposed to even look at the second one. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac = computeMAC(
+ t, key, dpath.InfoFields[1], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac = computeMAC(
+ t, otherKey, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ ingress := uint16(1) // from peering link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+
+ // ... The SegID accumulator wasn't updated from HF[1],
+ // it is still the same. That is the key behavior.
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit peering non consdir": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet lands on the last (peering) hop of
+ // segment 0. After processing, the packet is ready to
+ // be processed by the first (peering) hop of segment 1.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 0,
+ SegLen: [3]uint8{6, 3, 0},
+ },
+ NumINF: 2,
+ NumLines: 9,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true},
+ // down seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ },
+ }
+
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (0 and 1) derive from the same SegID
+ // accumulator value. However, the forwarding code isn't
+ // supposed to even look at the first one. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[0].HopField.Mac =
+ computeMAC(t, otherKey, dpath.InfoFields[0], dpath.HopFields[0].HopField)
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+
+ // We're going against construction order, so the accumulator
+ // value is that of the previous hop in traversal order. The
+ // story starts with the packet arriving at hop 1, so the
+ // accumulator value must match hop field 0. In this case,
+ // it is identical to that for hop field 1, which we made
+ // identical to the original SegID. So, we're all set.
+ ingress := uint16(2) // from child link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+
+ // The SegID should not get updated on arrival. If it is, then MAC validation
+ // of HF1 will fail. Otherwise, this isn't visible because we changed segment.
+ egress = 1
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "peering consdir downstream": {
+ // Similar to previous test case but looking at what
+ // happens on the next hop.
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet just left hop 1 (the first hop
+ // of peering down segment 1) and is processed at hop 2
+ // which is not a peering hop.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 6,
+ CurrINF: 1,
+ SegLen: [3]uint8{3, 9, 0},
+ },
+ NumINF: 2,
+ NumLines: 12,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ // core seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ // There has to be a 4th hop to make
+ // the 3rd router agree that the packet
+ // is not at destination yet.
+ },
+ }
+
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. The router shouldn't need to
+ // know this or do anything special. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, otherKey, dpath.InfoFields[1], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ ingress := uint16(1)
+ egress := uint16(0)
+ // The SegID we provide is that of HF[2] which happens to be SEG[1]'s SegID,
+ // so, already set for the before-processing state.
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+
+ // ... The SegID accumulator should have been updated.
+ dpath.InfoFields[1].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "peering non consdir upstream": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet lands on the second (non-peering) hop of
+ // segment 0 (a peering segment). After processing, the packet
+ // is ready to be processed by the third (peering) hop of segment 0.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 0,
+ SegLen: [3]uint8{9, 3, 0},
+ },
+ NumINF: 2,
+ NumLines: 12,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true},
+ // down seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ // The second segment (4th hop) has to be
+ // there but the packet isn't processed
+ // at that hop for this test.
+ },
+ }
+
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. The SegID accumulator value can
+ // be anything (it comes from the parent hop of HF[1]
+ // in the original beaconned segment, which is not in
+ // the path). So, we use one from an info field because
+ // computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, otherKey, dpath.InfoFields[0], dpath.HopFields[2].HopField)
+
+ ingress := uint16(2) // from child link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.HopLines))
+
+ // After-processing, the SegID should have been updated
+ // (on ingress) to be that of HF[1], which happens to be
+ // the Segment's SegID. That is what we already have as
+ // we only change it in the before-processing version
+ // of the packet.
+ egress = 1
+ } else {
+ // We're going against construction order, so the before-processing accumulator
+ // value is that of the previous hop in traversal order. The story starts with
+ // the packet arriving at hop 1, so the accumulator value must match hop field
+ // 0, which derives from hop field[1]. HopField[0]'s MAC is not checked during
+ // this test.
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "astransit direct": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{1}, // Interface 3 is in the external interfaces of a sibling router
+ map[uint16]topology.LinkType{
+ 1: topology.Core,
+ 3: topology.Core,
+ },
+ nil, // No special connOpener.
+ map[uint16]netip.AddrPort{
+ uint16(3): netip.MustParseAddrPort("10.0.200.200:30043"),
+ }, addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 3}},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ }
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ var dstAddr *net.UDPAddr
+ ingress := uint16(1)
+ egress := uint16(0) // To make sure it gets updated.
+ if afterProcessing {
+ egress = uint16(3) // The sibling router is locally mapped to the egress ifID.
+ // The link is specific to the sibling. It has the address. So we don't expect:
+ // dstAddr = &net.UDPAddr{IP: net.ParseIP("10.0.200.200").To4(), Port: 30043}
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "astransit xover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDP(
+ []uint16{51},
+ map[uint16]topology.LinkType{
+ 51: topology.Child,
+ 3: topology.Core,
+ },
+ nil, // No special connOpener.
+ map[uint16]netip.AddrPort{
+ uint16(3): netip.MustParseAddrPort("10.0.200.200:30043"),
+ }, addr.MustParseIA("1-ff00:0:110"), nil, key)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 3,
+ SegLen: [3]uint8{6, 6, 0},
+ },
+ NumINF: 2,
+ NumLines: 12,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ // core seg
+ {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 0}}, // Src,
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 51}}, // IA 110
+ {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}}, // IA 110
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1}}, // Dst
+ },
+ }
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+
+ var dstAddr *net.UDPAddr
+ ingress := uint16(51) // == consEgress, bc non-consdir
+ egress := uint16(0) // To check that it is updated
+ if afterProcessing {
+ require.NoError(t, dpath.IncPath(hummingbird.HopLines))
+ egress = uint16(3) // Internal hop => egress points at sibling router.
+ // The link is specific to the sibling. It has the address. So we don't expect:
+ // dstAddr = &net.UDPAddr{IP: net.ParseIP("10.0.200.200").To4(), Port: 30043}
+ } else {
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+ }
+
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "inbound flyover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ mockExternalInterfaces,
+ nil,
+ nil,
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ spkt.DstIA = addr.MustParseIA("1-ff00:0:110")
+ dst := addr.MustParseHost("10.0.100.100")
+ assert.NoError(t, spkt.SetDstAddr(dst))
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 41, ConsEgress: 40}},
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 0},
+ Flyover: true, ResStartTime: 123, Duration: 304, Bw: 16},
+ }
+ dpath.Base.PathMeta.SegLen[0] = 6 + 5 // 2 hops + 1 flyover
+ dpath.Base.NumLines = 6 + 5
+ dpath.Base.PathMeta.CurrHF = 6
+ dpath.HopFields[2].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[2], dpath.PathMeta)
+ var dstAddr *net.UDPAddr
+ ingress := uint16(1)
+ egress := uint16(0)
+ if afterProcessing {
+ dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+ dpath.HopFields[2].HopField)
+ dstAddr = &net.UDPAddr{IP: dst.IP().AsSlice(), Port: dstUDPPort}
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "outbound flyover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1},
+ map[uint16]topology.LinkType{
+ 1: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ spkt.SrcIA = addr.MustParseIA("1-ff00:0:110")
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1},
+ Flyover: true, ResStartTime: 123, Duration: 304, Bw: 16},
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30},
+ Flyover: true, ResStartTime: 123, Duration: 304, Bw: 16},
+ {HopField: path.HopField{ConsIngress: 41, ConsEgress: 40},
+ Flyover: true, ResStartTime: 123, Duration: 304, Bw: 16},
+ }
+ dpath.Base.PathMeta.CurrHF = 0
+ dpath.Base.PathMeta.SegLen[0] = 5 * 3 // 3 flyovers
+ dpath.NumLines = 15
+ dpath.HopFields[0].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[0], dpath.Base.PathMeta)
+ ingress := uint16(0)
+ egress := uint16(0)
+ if afterProcessing {
+ dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+ dpath.HopFields[0].HopField)
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+ egress = 1
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "reservation expired": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1},
+ map[uint16]topology.LinkType{
+ 1: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ spkt.SrcIA = addr.MustParseIA("1-ff00:0:110")
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 41, ConsEgress: 40}},
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1},
+ Flyover: true, ResStartTime: 5, Duration: 2, Bw: 16},
+ }
+ dpath.Base.PathMeta.SegLen[0] = 6 + 5 // 1 flyover
+ dpath.NumLines = 11
+ dpath.Base.PathMeta.CurrHF = 6
+ dpath.HopFields[0].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[2], dpath.Base.PathMeta)
+ ingress := uint16(0)
+ egress := uint16(0)
+ if afterProcessing {
+ dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+ dpath.HopFields[0].HopField)
+ egress = 1
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: discarded,
+ },
+ "brtransit flyover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Parent,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+ Flyover: true, Bw: 5, ResStartTime: 123, Duration: 304},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ }
+
+ dpath.Base.PathMeta.SegLen[0] = 11 // 1 flyover
+ dpath.Base.NumLines = 11
+ dpath.Base.PathMeta.CurrHF = 3
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+ ingress := uint16(1)
+ egress := uint16(2)
+ if afterProcessing {
+ dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+ dpath.HopFields[1].HopField)
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit non consdir flyover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 2: topology.Parent,
+ 1: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 2, ConsEgress: 1},
+ Flyover: true, ResID: 42, ResStartTime: 5, Duration: 301, Bw: 16},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ }
+ dpath.Base.NumLines = 11
+ dpath.Base.PathMeta.SegLen[0] = 11
+ dpath.Base.PathMeta.CurrHF = 3
+ dpath.InfoFields[0].ConsDir = false
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.PathMeta)
+ ingress := uint16(1)
+ egress := uint16(2)
+ if afterProcessing {
+ dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+ dpath.HopFields[1].HopField)
+ require.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ } else {
+ // Against construction direction.
+ dpath.InfoFields[0].UpdateSegID(
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField))
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "astransit direct flyover": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1}, // Interface 3 is in the external interfaces of a sibling router
+ map[uint16]topology.LinkType{
+ 1: topology.Core,
+ 3: topology.Core,
+ },
+ nil, // No special connOpener.
+ map[uint16]netip.AddrPort{
+ uint16(3): netip.MustParseAddrPort("10.0.200.200:30043"),
+ }, addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, dpath := prepHbirdMsg(now)
+ dpath.HopFields = []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 3},
+ Flyover: true, ResID: 42, ResStartTime: 5, Duration: 301, Bw: 16},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ }
+ dpath.Base.NumLines = 11
+ dpath.Base.PathMeta.SegLen[0] = 11
+ dpath.PathMeta.CurrHF = 3
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+ var dstAddr *net.UDPAddr
+ ingress := uint16(1)
+ egress := uint16(3)
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "astransit xover flyover ingress": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{51},
+ map[uint16]topology.LinkType{
+ 51: topology.Child,
+ 3: topology.Core,
+ },
+ nil, // No special connOpener.
+ map[uint16]netip.AddrPort{
+ uint16(3): netip.MustParseAddrPort("10.0.200.200:30043"),
+ }, addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ NumINF: 2,
+ NumLines: 9 + 5, // 1 flyover
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 0,
+ CurrHF: 3,
+ SegLen: [3]uint8{8, 6, 0}, // Flyover on first segment
+ BaseTS: util.TimeToSecs(now),
+ },
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ // core seg
+ {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 0}}, // Src,
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 51},
+ Flyover: true, Bw: 5, ResStartTime: 5, Duration: 310}, // IA 110
+ // xover here.
+ {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}}, // IA 110
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1}}, // Dst
+ },
+ }
+ dpath.HopFields[1].HopField.Mac = computeAggregateMacExplicitInEg(
+ t, key, hbirdKey, spkt.DstIA, spkt.PayloadLen, 3, 51,
+ dpath.InfoFields[0], dpath.HopFields[1], dpath.PathMeta)
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ var dstAddr *net.UDPAddr
+ ingress := uint16(51) // == consEgress, bc non-consdir
+ egress := uint16(0) // To check that it is updated
+ if afterProcessing {
+ dpath.HopFields[1].Flyover = false
+ dpath.HopFields[2].Flyover = true
+ dpath.HopFields[2].Bw = 5
+ dpath.HopFields[2].ResStartTime = 5
+ dpath.HopFields[2].Duration = 310
+ dpath.HopFields[1].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac = computeAggregateMacExplicitInEg(t, key, hbirdKey,
+ spkt.DstIA, spkt.PayloadLen, 3, 51,
+ dpath.InfoFields[1], dpath.HopFields[2], dpath.PathMeta)
+ dpath.PathMeta.SegLen[0] -= 2
+ dpath.PathMeta.SegLen[1] += 2
+ require.NoError(t, dpath.IncPath(hummingbird.HopLines))
+ egress = uint16(3) // Internal hop => egress points at sibling router.
+ // The link is specific to the sibling. It has the address. So we don't expect:
+ // dstAddr = &net.UDPAddr{IP: net.ParseIP("10.0.200.200").To4(), Port: 30043}
+ } else {
+ // The BR is going to update the segment ID based on the regular SCION MAC,
+ // not the flyover one. Since both are XOR-aggregated into the mac field,
+ // we need to de-aggregate the flyover first.
+ dpath.InfoFields[0].UpdateSegID(deaggregateFlyoverFromMac(
+ t,
+ key,
+ dpath.InfoFields[0],
+ dpath.HopFields[1],
+ ))
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, dstAddr, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "astransit xover flyover egress": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{3},
+ map[uint16]topology.LinkType{
+ 51: topology.Child,
+ 3: topology.Core,
+ },
+ nil, // No special connOpener.
+ map[uint16]netip.AddrPort{
+ uint16(51): netip.MustParseAddrPort("10.0.200.200:30043"),
+ }, addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, dp *router.DataPlane) *router.Packet {
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ NumINF: 2,
+ NumLines: 9 + 5, // 1 flyover
+ PathMeta: hummingbird.MetaHdr{
+ CurrINF: 1,
+ CurrHF: 6,
+ SegLen: [3]uint8{6, 8, 0}, // Flyover on second segment
+ BaseTS: util.TimeToSecs(now),
+ },
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ // core seg
+ {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 0}}, // Src,
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 51}}, // IA 110
+ // xover here.
+ {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}, // IA 110
+ Flyover: true, Bw: 5, ResStartTime: 5, Duration: 310},
+ {HopField: path.HopField{ConsIngress: 0, ConsEgress: 1}}, // Dst
+ },
+ }
+ dpath.HopFields[2].HopField.Mac = computeAggregateMacExplicitInEg(
+ t, key, hbirdKey, spkt.DstIA, spkt.PayloadLen, 3, 51,
+ dpath.InfoFields[1], dpath.HopFields[2], dpath.PathMeta)
+ ingress := uint16(0) // from sibling router
+ egress := uint16(3)
+ if afterProcessing {
+ // Restore flyover to xover ingress hop from the egress one.
+ dpath.HopFields[2].Flyover = false
+ dpath.HopFields[1].Flyover = true
+ dpath.HopFields[1].Bw = 5
+ dpath.HopFields[1].ResStartTime = 5
+ dpath.HopFields[1].Duration = 310
+ dpath.HopFields[2].HopField.Mac =
+ computeMAC(t, key, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ dpath.PathMeta.SegLen[0] += 2
+ dpath.PathMeta.SegLen[1] -= 2
+ require.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ }
+ pkt := router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ // Replace the link of the packet with the one from dataplane.
+ ifaces := router.ExtractInterfaces(dp)
+ // At the xover egress border router, the packet enters the BR via 0, but the
+ // sibling border router link is stored at the ingress on the AS (previous hop).
+ pkt.Link = ifaces[51]
+ return pkt
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit peering consdir flyovers": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet just left segment 0 which ends at
+ // (peering) hop 0 and is landing on segment 1 which
+ // begins at (peering) hop 1. We do not care what hop 0
+ // looks like. The forwarding code is looking at hop 1 and
+ // should leave the message in shape to be processed at hop 2.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 1,
+ SegLen: [3]uint8{3, 8, 0},
+ BaseTS: util.TimeToSecs(now),
+ },
+ NumINF: 2,
+ NumLines: 11,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ // core seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+ Flyover: true, Bw: 5, ResStartTime: 123, Duration: 304},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ },
+ }
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. However, the forwarding code isn't
+ // supposed to even look at the second one. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[1], dpath.HopFields[1], dpath.PathMeta)
+ dpath.HopFields[2].HopField.Mac = computeMAC(
+ t, hbirdKey, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ ingress := uint16(1) // from peering link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ // deaggregate MAC
+ dpath.HopFields[1].HopField.Mac = computeMAC(
+ t, key, dpath.InfoFields[1], dpath.HopFields[1].HopField)
+ // ... The SegID accumulator wasn't updated from HF[1],
+ // it is still the same. That is the key behavior.
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "brtransit peering non consdir flyovers": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet lands on the last (peering) hop of
+ // segment 0. After processing, the packet is ready to
+ // be processed by the first (peering) hop of segment 1.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 0,
+ SegLen: [3]uint8{8, 3, 0},
+ BaseTS: util.TimeToSecs(now),
+ },
+ NumINF: 2,
+ NumLines: 11,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true},
+ // down seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+ Flyover: true, Bw: 5, ResStartTime: 123, Duration: 304},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ },
+ }
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (0 and 1) derive from the same SegID
+ // accumulator value. However, the forwarding code isn't
+ // supposed to even look at the first one. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[0].HopField.Mac = computeMAC(
+ t, hbirdKey, dpath.InfoFields[0], dpath.HopFields[0].HopField)
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.PathMeta)
+ // We're going against construction order, so the accumulator
+ // value is that of the previous hop in traversal order. The
+ // story starts with the packet arriving at hop 1, so the
+ // accumulator value must match hop field 0. In this case,
+ // it is identical to that for hop field 1, which we made
+ // identical to the original SegID. So, we're all set.
+ ingress := uint16(2) // from child link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ // Deaggregate MAC.
+ dpath.HopFields[1].HopField.Mac = computeMAC(
+ t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ // The SegID should not get updated on arrival. If it is, then MAC validation
+ // of HF1 will fail. Otherwise, this isn't visible because we changed segment.
+ egress = 1
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "peering consdir downstream flyovers": {
+ // Similar to previous test case but looking at what
+ // happens on the next hop.
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet just left hop 1 (the first hop
+ // of peering down segment 1) and is processed at hop 2
+ // which is not a peering hop.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 6,
+ CurrINF: 1,
+ SegLen: [3]uint8{1, 11, 0},
+ BaseTS: util.TimeToSecs(now),
+ },
+ NumINF: 2,
+ NumLines: 14,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ // core seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+ Flyover: true, Bw: 5, ResStartTime: 123, Duration: 304},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ // There has to be a 4th hop to make
+ // the 3rd router agree that the packet
+ // is not at destination yet.
+ },
+ }
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. The router shouldn't need to
+ // know this or do anything special. The SegID
+ // accumulator value can be anything (it comes from the
+ // parent hop of HF[1] in the original beaconned segment,
+ // which is not in the path). So, we use one from an
+ // info field because computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac = computeMAC(
+ t, hbirdKey, dpath.InfoFields[1], dpath.HopFields[1].HopField)
+ dpath.HopFields[2].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[1], dpath.HopFields[2], dpath.PathMeta)
+ ingress := uint16(1)
+ egress := uint16(0)
+ // The SegID we provide is that of HF[2] which happens to be SEG[1]'s SegID,
+ // so, already set for the before-processing state.
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ // Deaggregate MAC.
+ dpath.HopFields[2].HopField.Mac = computeMAC(
+ t, key, dpath.InfoFields[1], dpath.HopFields[2].HopField)
+ // ... The SegID accumulator should have been updated.
+ dpath.InfoFields[1].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+ egress = 2
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ "peering non consdir upstream flyovers": {
+ prepareDP: func(ctrl *gomock.Controller) *router.DataPlane {
+ return router.NewDPWithHummingbirdKey(
+ []uint16{1, 2},
+ map[uint16]topology.LinkType{
+ 1: topology.Peer,
+ 2: topology.Child,
+ },
+ nil, // No special connOpener.
+ mockInternalNextHops,
+ addr.MustParseIA("1-ff00:0:110"), nil, key, hbirdKey)
+ },
+ mockMsg: func(afterProcessing bool, _ *router.DataPlane) *router.Packet {
+ // Story: the packet lands on the second (non-peering) hop of
+ // segment 0 (a peering segment). After processing, the packet
+ // is ready to be processed by the third (peering) hop of segment 0.
+ spkt, _ := prepHbirdMsg(now)
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ CurrINF: 0,
+ SegLen: [3]uint8{11, 3, 0},
+ BaseTS: util.TimeToSecs(now),
+ },
+ NumINF: 2,
+ NumLines: 14,
+ },
+ InfoFields: []path.InfoField{
+ // up seg
+ {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true},
+ // down seg
+ {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true},
+ },
+ HopFields: []hummingbird.FlyoverHopField{
+ {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+ {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+ Flyover: true, Bw: 5, ResStartTime: 123, Duration: 304},
+ {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+ {HopField: path.HopField{ConsIngress: 50, ConsEgress: 51}},
+ // The second segment (4th hop) has to be
+ // there but the packet isn't processed
+ // at that hop for this test.
+ },
+ }
+ // Make obvious the unusual aspect of the path: two
+ // hopfield MACs (1 and 2) derive from the same SegID
+ // accumulator value. The SegID accumulator value can
+ // be anything (it comes from the parent hop of HF[1]
+ // in the original beaconned segment, which is not in
+ // the path). So, we use one from an info field because
+ // computeMAC makes that easy.
+ dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, hbirdKey, spkt.DstIA,
+ spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.PathMeta)
+ dpath.HopFields[2].HopField.Mac = computeMAC(
+ t, hbirdKey, dpath.InfoFields[0], dpath.HopFields[2].HopField)
+ ingress := uint16(2) // from child link
+ egress := uint16(0)
+ if afterProcessing {
+ assert.NoError(t, dpath.IncPath(hummingbird.FlyoverLines))
+ // Deaggregate MAC.
+ dpath.HopFields[1].HopField.Mac = computeMAC(
+ t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ // After-processing, the SegID should have been updated
+ // (on ingress) to be that of HF[1], which happens to be
+ // the Segment's SegID. That is what we already have as
+ // we only change it in the before-processing version
+ // of the packet.
+ egress = 1
+ } else {
+ // We're going against construction order, so the before-processing accumulator
+ // value is that of the previous hop in traversal order. The story starts with
+ // the packet arriving at hop 1, so the accumulator value must match hop field
+ // 0, which derives from hop field[1]. HopField[0]'s MAC is not checked during
+ // this test.
+ // Use de-aggregated MAC value for segID update
+ scionMac := computeMAC(
+ t, key, dpath.InfoFields[0], dpath.HopFields[1].HopField)
+ dpath.InfoFields[0].UpdateSegID(scionMac)
+ }
+ return router.NewPacket(toBytes(t, spkt, dpath), nil, nil, ingress, egress)
+ },
+ assertFunc: notDiscarded,
+ },
+ }
+
+ for name, tc := range testCases {
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ dp := tc.prepareDP(ctrl)
+ pkt, want := tc.mockMsg(false, dp), tc.mockMsg(true, dp)
+ disp := dp.ProcessPkt(pkt)
+ tc.assertFunc(t, disp)
+ if disp == router.PDiscard {
+ return
+ }
+ assertPktEqual(t, want, pkt)
+ })
+ }
+}
+
+// func TestHbirdPacketPath(t *testing.T) {
+// ctrl := gomock.NewController(t)
+// defer ctrl.Finish()
+// key := []byte("testkey_xxxxxxxx")
+// sv := []byte("test_secretvalue")
+// now := time.Now()
+// testCases := map[string]struct {
+// mockMsg func() *ipv4.Message
+// prepareDPs func(*gomock.Controller) []*router.DataPlane
+// srcInterfaces []uint16
+// }{
+// "two hops consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("1-ff00:0:110"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{6, 0, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 1,
+// NumLines: 6,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// {HopField: path.HopField{ConsIngress: 01, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [2]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(01): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 01: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 01},
+// },
+// "two hops non consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:111"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{6, 0, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 1,
+// NumLines: 6,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 01, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// //dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [2]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(01): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 01: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 40},
+// },
+// "six hops astransit xover consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{9, 9, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 18,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// {HopField: path.HopField{ConsIngress: 1, ConsEgress: 31}},
+// {HopField: path.HopField{ConsIngress: 5, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 7}},
+// {HopField: path.HopField{ConsIngress: 11, ConsEgress: 8}},
+// {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// dpath.InfoFields[1].SegID = 0x222
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [7]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(5): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(11): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[6] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 0, 11, 0, 3},
+// },
+// "six hops astransit xover non consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{9, 9, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 18,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 1}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 5}},
+// {HopField: path.HopField{ConsIngress: 7, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 8, ConsEgress: 11}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 3}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[5].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// // Reset SegID to original value
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [7]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(5): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(11): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[6] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 0, 11, 0, 3},
+// },
+// "six hops brtransit xover mixed consdir": {
+// // up segment non consdir, down segment consdir
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{9, 9, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 18,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 1}},
+// {HopField: path.HopField{ConsIngress: 7, ConsEgress: 5}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 7}},
+// {HopField: path.HopField{ConsIngress: 11, ConsEgress: 8}},
+// {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[1].SegID = 0x222
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [5]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 11: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 11, 3},
+// },
+// "six hops three segs mixed consdir": {
+// // two crossovers, first crossover is brtransit, second one is astransit
+// // core segment is non consdir
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{6, 6, 6},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 3,
+// NumLines: 18,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x333, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// {HopField: path.HopField{ConsIngress: 1, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 5, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 31}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 8}},
+// {HopField: path.HopField{ConsIngress: 3, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[2],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[2].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[2],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// dpath.InfoFields[2].SegID = 0x333
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [5]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Child,
+// 5: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 31: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 31: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 31, 0, 3},
+// },
+// "three hops peering brtransit consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{3, 6},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 9,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+// {HopField: path.HopField{ConsIngress: 5, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[1].HopField)
+// // No Segment update here as the second hop of a peering path
+// // Uses the same segID as it's following hop
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [3]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Peer,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 5},
+// },
+// "three hops peering brtransit non consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{3, 6},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 9,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 2, ConsEgress: 1}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 5}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[1].HopField)
+// // No Segment update here as the second hop of a peering path
+// // Uses the same segID as it's following hop
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [3]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Peer,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 5},
+// },
+// "four hops peering astransit consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{6, 6},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 12,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 40}},
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 7}},
+// {HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}},
+// {HopField: path.HopField{ConsIngress: 5, ConsEgress: 0}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// // No Segment update here
+// // the second hop of a peering path uses the same segID as it's following hop
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// // reset segID
+// dpath.InfoFields[0].SegID = 0x111
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [6]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(2): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(1): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 31, 0, 1, 0, 5},
+// },
+// "four hops peering astransit non consdir": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{6, 6},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 12,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 0}},
+// {HopField: path.HopField{ConsIngress: 7, ConsEgress: 31}},
+// {HopField: path.HopField{ConsIngress: 2, ConsEgress: 1}},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 5}},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// // No Segment update here
+// // the second hop of a peering path uses the same segID as it's following hop
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [6]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(2): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(1): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 31, 0, 1, 0, 5},
+// },
+// "two hops consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("1-ff00:0:110"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{10, 0, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 1,
+// NumLines: 10,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 01, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// // add flyover macs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 0,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [2]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(01): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 01: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 01},
+// },
+// "two hops non consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:111"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{10, 0, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 1,
+// NumLines: 10,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 01, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// // aggregate macs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 1,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 40, 0,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [2]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(01): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 01: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 40},
+// },
+// "six hops astransit xover consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{15, 13, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 28,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 31},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 5, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 7}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 11, ConsEgress: 8},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 3, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// dpath.InfoFields[1].SegID = 0x222
+// // aggregate flyover macs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 31,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 7,
+// dpath.InfoFields[0], &dpath.HopFields[2], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 11, 8,
+// dpath.InfoFields[1], &dpath.HopFields[4], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 3, 0,
+// dpath.InfoFields[1], &dpath.HopFields[5], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [7]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(5): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(11): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[6] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 0, 11, 0, 3},
+// },
+// "six hops astransit xover non consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{15, 13, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 28,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 40, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 31, ConsEgress: 1},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 5},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 7, ConsEgress: 0}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 8, ConsEgress: 11},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 3},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[5].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// // aggregate with flyover macs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 31,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 7,
+// dpath.InfoFields[0], &dpath.HopFields[2], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 11, 8,
+// dpath.InfoFields[1], &dpath.HopFields[4], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 3, 0,
+// dpath.InfoFields[1], &dpath.HopFields[5], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [7]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(5): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Core,
+// 11: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(11): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[6] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 0, 11, 0, 3},
+// },
+// "six hops brtransit xover mixed consdir flyovers": {
+// // up segment non consdir, down segment consdir
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:111"),
+// xtest.MustParseIA("3-ff00:0:333"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{15, 13, 0},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 28,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 40, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 31, ConsEgress: 1},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 5},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 7}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 11, ConsEgress: 8},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 3, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].HopField.Mac)
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[1].SegID = 0x222
+// //aggregate MACs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 31,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 7,
+// dpath.InfoFields[0], &dpath.HopFields[2], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 11, 8,
+// dpath.InfoFields[1], &dpath.HopFields[4], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 3, 0,
+// dpath.InfoFields[1], &dpath.HopFields[5], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [5]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 31: topology.Parent,
+// 1: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Child,
+// 7: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// uint16(11): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 11: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("2-ff00:0:222"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("3-ff00:0:333"), nil, key, sv)
+// return dps[:]
+// }, // middle hop of second segment is astransit
+// srcInterfaces: []uint16{0, 1, 5, 11, 3},
+// },
+// "six hops three segs mixed consdir flyovers": {
+// // two crossovers, first crossover is brtransit, second one is astransit
+// // core segment is non consdir
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{10, 8, 8},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 3,
+// NumLines: 26,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x333, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 5, ConsEgress: 0}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 31},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 0, ConsEgress: 8}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 3, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// dpath.HopFields[4].HopField.Mac = computeMAC(t, key, dpath.InfoFields[2],
+// dpath.HopFields[4].HopField)
+// dpath.InfoFields[2].UpdateSegID(dpath.HopFields[4].HopField.Mac)
+// dpath.HopFields[5].HopField.Mac = computeMAC(t, key, dpath.InfoFields[2],
+// dpath.HopFields[5].HopField)
+// // Reset SegID to original value
+// dpath.InfoFields[0].SegID = 0x111
+// dpath.InfoFields[2].SegID = 0x333
+// // aggregate flyover macs
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 5,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 31, 8,
+// dpath.InfoFields[1], &dpath.HopFields[3], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 3, 0,
+// dpath.InfoFields[2], &dpath.HopFields[5], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [5]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Child,
+// 5: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 31: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(8): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(8): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 8: topology.Child,
+// 31: topology.Core,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 3: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 31, 0, 3},
+// },
+// "three hops peering brtransit consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{5, 10},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 15,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 5, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[1].HopField)
+// // No Segment update here
+// // The second hop of a peering path uses the same segID as it's following hop
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 2,
+// dpath.InfoFields[1], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 0,
+// dpath.InfoFields[1], &dpath.HopFields[2], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [3]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Peer,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 5},
+// },
+// "three hops peering brtransit non consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{5, 10},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 15,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 40, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 2, ConsEgress: 1},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 5},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[2].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[1].HopField)
+// // No Segment update here
+// // The second hop of a peering path uses the same segID as it's following hop
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 2,
+// dpath.InfoFields[1], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 0,
+// dpath.InfoFields[1], &dpath.HopFields[2], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [3]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Peer,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 1, 5},
+// },
+// "four hops peering astransit consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{10, 10},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 20,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 40},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 31, ConsEgress: 7},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 2},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 5, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.InfoFields[0].UpdateSegID(dpath.HopFields[0].HopField.Mac)
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// // No Segment update here
+// // The second hop of a peering path uses the same segID as it's following hop
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// // reset segID
+// dpath.InfoFields[0].SegID = 0x111
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 31, 7,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 2,
+// dpath.InfoFields[1], &dpath.HopFields[2], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 0,
+// dpath.InfoFields[1], &dpath.HopFields[3], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [6]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(2): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(1): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 31, 0, 1, 0, 5},
+// },
+// "four hops peering astransit non consdir flyovers": {
+// mockMsg: func() *ipv4.Message {
+// spkt := prepHbirdSlayers(xtest.MustParseIA("1-ff00:0:110"),
+// xtest.MustParseIA("1-ff00:0:113"))
+// dst := addr.MustParseHost("10.0.100.100")
+// _ = spkt.SetDstAddr(dst)
+// dpath := &hummingbird.Decoded{
+// Base: hummingbird.Base{
+// PathMeta: hummingbird.MetaHdr{
+// CurrINF: 0,
+// CurrHF: 0,
+// SegLen: [3]uint8{10, 10},
+// BaseTS: util.TimeToSecs(now),
+// HighResTS: 500 << 22,
+// },
+// NumINF: 2,
+// NumLines: 20,
+// },
+// InfoFields: []path.InfoField{
+// {SegID: 0x111, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// {SegID: 0x222, Peer: true, ConsDir: false, Timestamp: util.TimeToSecs(now)},
+// },
+// HopFields: []hummingbird.FlyoverHopField{
+// {Flyover: true, HopField: path.HopField{ConsIngress: 40, ConsEgress: 0},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 7, ConsEgress: 31},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 2, ConsEgress: 1},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 0, ConsEgress: 5},
+// Bw: 5, ResStartTime: 123, Duration: 304},
+// },
+// }
+// // Compute MACs and increase SegID while doing so
+// dpath.HopFields[1].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[1].HopField)
+// // No Segment update here
+// // The second hop of a peering path uses the same segID as it's following hop
+// dpath.HopFields[0].HopField.Mac = computeMAC(t, key, dpath.InfoFields[0],
+// dpath.HopFields[0].HopField)
+// dpath.HopFields[3].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[3].HopField)
+// dpath.InfoFields[1].UpdateSegID(dpath.HopFields[3].HopField.Mac)
+// dpath.HopFields[2].HopField.Mac = computeMAC(t, key, dpath.InfoFields[1],
+// dpath.HopFields[2].HopField)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 0, 40,
+// dpath.InfoFields[0], &dpath.HopFields[0], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 31, 7,
+// dpath.InfoFields[0], &dpath.HopFields[1], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 1, 2,
+// dpath.InfoFields[1], &dpath.HopFields[2], dpath.PathMeta)
+// aggregateOntoScionMac(t, sv, spkt.DstIA, spkt.PayloadLen, 5, 0,
+// dpath.InfoFields[1], &dpath.HopFields[3], dpath.PathMeta)
+// ret := toMsg(t, spkt, dpath)
+// return ret
+// },
+// prepareDPs: func(*gomock.Controller) []*router.DataPlane {
+// var dps [6]*router.DataPlane
+// dps[0] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(40): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 40: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+// dps[1] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(31): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(7): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[2] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(7): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 7: topology.Peer,
+// 31: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(31): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:111"), nil, key, sv)
+// dps[3] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(1): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(2): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[4] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Peer,
+// 2: topology.Child,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// map[uint16]*net.UDPAddr{
+// uint16(1): {IP: net.ParseIP("10.0.200.200").To4(), Port: 30043},
+// }, nil, xtest.MustParseIA("1-ff00:0:112"), nil, key, sv)
+// dps[5] = router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(5): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 5: topology.Parent,
+// },
+// mock_router.NewMockBatchConn(ctrl),
+// nil, nil, xtest.MustParseIA("1-ff00:0:113"), nil, key, sv)
+// return dps[:]
+// },
+// srcInterfaces: []uint16{0, 31, 0, 1, 0, 5},
+// },
+// }
+// for name, tc := range testCases {
+// name, tc := name, tc
+// t.Run(name, func(t *testing.T) {
+// t.Parallel()
+// dps := tc.prepareDPs(ctrl)
+// input := tc.mockMsg()
+// for i, dp := range dps {
+// result, err := dp.ProcessPkt(tc.srcInterfaces[i], input)
+// assert.NoError(t, err)
+// input = &ipv4.Message{
+// Buffers: [][]byte{result.OutPkt},
+// Addr: result.OutAddr,
+// N: len(result.OutPkt),
+// }
+// }
+// })
+// }
+// }
+
+// TODO(juagargi): write test for concurrent bandwidth check calls
+
+// func TestBandwidthCheck(t *testing.T) {
+// ctrl := gomock.NewController(t)
+// defer ctrl.Finish()
+
+// key := []byte("testkey_xxxxxxxx")
+// sv := []byte("test_secretvalue")
+// now := time.Now()
+
+// dp := router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Parent,
+// 2: topology.Child,
+// },
+// nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+
+// spkt, dpath := prepHbirdMsg(now)
+// dpath.HopFields = []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}, ResID: 42,
+// Bw: 2, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+// }
+// dpath.Base.PathMeta.SegLen[0] = 11
+// dpath.Base.PathMeta.CurrHF = 3
+// dpath.Base.NumLines = 11
+
+// spkt.PayloadLen = 120
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+
+// msg := toLongMsg(t, spkt, dpath)
+
+// _, err := dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.Error(t, err)
+
+// time.Sleep(time.Duration(1) * time.Second)
+
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+// }
+
+// func TestBandwidthCheckDifferentResID(t *testing.T) {
+// // Verifies that packets of one reservation do not affect
+// // available bandwidth of another reservation
+// ctrl := gomock.NewController(t)
+// defer ctrl.Finish()
+
+// key := []byte("testkey_xxxxxxxx")
+// sv := []byte("test_secretvalue")
+// now := time.Now()
+
+// dp := router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Parent,
+// 2: topology.Child,
+// },
+// nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+
+// spkt, dpath := prepHbirdMsg(now)
+// dpath.HopFields = []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}, ResID: 24,
+// Bw: 2, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+// }
+// dpath.Base.PathMeta.SegLen[0] = 11
+// dpath.Base.PathMeta.CurrHF = 3
+// dpath.Base.NumLines = 11
+
+// spkt.PayloadLen = 120
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+
+// msg := toLongMsg(t, spkt, dpath)
+
+// _, err := dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+
+// dpath.HopFields[1].ResID = 32
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+
+// dpath.HopFields[1].ResID = 42
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+// }
+
+// func TestBandwidthCheckDifferentEgress(t *testing.T) {
+// ctrl := gomock.NewController(t)
+// defer ctrl.Finish()
+
+// key := []byte("testkey_xxxxxxxx")
+// sv := []byte("test_secretvalue")
+// now := time.Now()
+
+// dp := router.NewDP(
+// map[uint16]router.BatchConn{
+// uint16(2): mock_router.NewMockBatchConn(ctrl),
+// uint16(3): mock_router.NewMockBatchConn(ctrl),
+// },
+// map[uint16]topology.LinkType{
+// 1: topology.Parent,
+// 2: topology.Child,
+// 3: topology.Child,
+// },
+// nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key, sv)
+
+// spkt, dpath := prepHbirdMsg(now)
+// dpath.HopFields = []hummingbird.FlyoverHopField{
+// {HopField: path.HopField{ConsIngress: 31, ConsEgress: 30}},
+// {Flyover: true, HopField: path.HopField{ConsIngress: 1, ConsEgress: 2}, ResID: 42,
+// Bw: 2, ResStartTime: 123, Duration: 304},
+// {HopField: path.HopField{ConsIngress: 40, ConsEgress: 41}},
+// }
+// dpath.Base.PathMeta.SegLen[0] = 11
+// dpath.Base.PathMeta.CurrHF = 3
+// dpath.Base.NumLines = 11
+
+// spkt.PayloadLen = 120
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+
+// msg := toLongMsg(t, spkt, dpath)
+
+// _, err := dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.Error(t, err)
+
+// // Reservation with same resID but different Ingress/Egress pair is a different reservation
+// dpath.HopFields[1].HopField.ConsEgress = 3
+// spkt.PayloadLen = 120
+// dpath.HopFields[1].HopField.Mac = computeAggregateMac(t, key, sv, spkt.DstIA,
+// spkt.PayloadLen, dpath.InfoFields[0], dpath.HopFields[1], dpath.Base.PathMeta)
+// msg = toLongMsg(t, spkt, dpath)
+// _, err = dp.ProcessPkt(1, msg)
+// assert.NoError(t, err)
+// }
+
+// func toLongMsg(t *testing.T, spkt *slayers.SCION, dpath path.Path) *ipv4.Message {
+// t.Helper()
+// ret := &ipv4.Message{}
+// spkt.Path = dpath
+// buffer := gopacket.NewSerializeBuffer()
+// payload := [120]byte{}
+// err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{FixLengths: true},
+// spkt, gopacket.Payload(payload[:]))
+// require.NoError(t, err)
+// raw := buffer.Bytes()
+// ret.Buffers = make([][]byte, 1)
+// ret.Buffers[0] = make([]byte, 1500)
+// copy(ret.Buffers[0], raw)
+// ret.N = len(raw)
+// ret.Buffers[0] = ret.Buffers[0][:ret.N]
+// return ret
+// }
+
+func prepHbirdMsg(now time.Time) (*slayers.SCION, *hummingbird.Decoded) {
+ spkt := &slayers.SCION{
+ Version: 0,
+ TrafficClass: 0xb8,
+ FlowID: 0xdead,
+ NextHdr: slayers.L4UDP,
+ PathType: hummingbird.PathType,
+ DstIA: addr.MustParseIA("4-ff00:0:411"),
+ SrcIA: addr.MustParseIA("2-ff00:0:222"),
+ Path: &hummingbird.Raw{},
+ PayloadLen: 26, // scionudpLayer + len("actualpayloadbytes")
+ }
+
+ dpath := &hummingbird.Decoded{
+ Base: hummingbird.Base{
+ PathMeta: hummingbird.MetaHdr{
+ CurrHF: 3,
+ SegLen: [3]uint8{9, 0, 0},
+ BaseTS: util.TimeToSecs(now),
+ HighResTS: 500 << 22,
+ },
+ NumINF: 1,
+ NumLines: 9,
+ },
+ InfoFields: []path.InfoField{
+ {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now)},
+ },
+
+ HopFields: []hummingbird.FlyoverHopField{},
+ }
+ return spkt, dpath
+}
+
+func prepHbirdSlayers(src, dst addr.IA) *slayers.SCION {
+ spkt := &slayers.SCION{
+ Version: 0,
+ TrafficClass: 0xb8,
+ FlowID: 0xdead,
+ NextHdr: slayers.L4UDP,
+ PathType: hummingbird.PathType,
+ DstIA: dst,
+ SrcIA: src,
+ Path: &hummingbird.Raw{},
+ PayloadLen: 26, // scionudpLayer + len("actualpayloadbytes")
+ }
+ return spkt
+}
+
+func computeAggregateMac(
+ t *testing.T,
+ key []byte,
+ sv []byte,
+ dst addr.IA,
+ l uint16,
+ info path.InfoField,
+ hf hummingbird.FlyoverHopField,
+ meta hummingbird.MetaHdr,
+) [path.MacLen]byte {
+ return computeAggregateMacExplicitInEg(
+ t, key, sv, dst, l, hf.HopField.ConsIngress, hf.HopField.ConsEgress,
+ info, hf, meta)
+}
+
+func computeAggregateMacExplicitInEg(
+ t *testing.T,
+ key []byte,
+ sv []byte,
+ dst addr.IA,
+ l uint16,
+ hin uint16,
+ heg uint16,
+ info path.InfoField,
+ hf hummingbird.FlyoverHopField,
+ meta hummingbird.MetaHdr,
+) [path.MacLen]byte {
+ scionMac := computeMAC(t, key, info, hf.HopField)
+
+ block, err := aes.NewCipher(sv)
+ require.NoError(t, err)
+ ingress, egress := hin, heg
+ if !info.ConsDir {
+ // deleteme since reservations are not bidirectional,
+ // specify here the exact ingress and egress that was used to make the reservation.
+ ingress, egress = egress, ingress
+ }
+
+ akBuffer := make([]byte, hummingbird.AkBufferSize)
+ macBuffer := make([]byte, hummingbird.FlyoverMacBufferSize)
+ xkBuffer := make([]uint32, hummingbird.XkBufferSize)
+
+ ak := hummingbird.DeriveAuthKey(block, hf.ResID, hf.Bw, ingress, egress,
+ meta.BaseTS-uint32(hf.ResStartTime), hf.Duration, akBuffer)
+ flyoverMac := hummingbird.FullFlyoverMac(ak, dst, l, hf.ResStartTime,
+ meta.HighResTS, macBuffer, xkBuffer)
+
+ for i, b := range scionMac {
+ scionMac[i] = b ^ flyoverMac[i]
+ }
+ return scionMac
+}
+
+// deaggregateFlyoverFromMac removes the flyover from the SCION MAC.
+func deaggregateFlyoverFromMac(
+ t *testing.T,
+ key []byte,
+ info path.InfoField,
+ flyover hummingbird.FlyoverHopField,
+) [6]byte {
+ scionMac := computeMAC(t, key, info, flyover.HopField) // Compute SCION MAC
+ mac := flyover.HopField.Mac // Copy MAC.
+ // MAC = S^F (SCION XOR Flyover).
+ mac[0] = (mac[0] ^ scionMac[0]) ^ mac[0] // S^F ^ S = F ; F ^ S^F = S
+ mac[1] = (mac[1] ^ scionMac[1]) ^ mac[1]
+
+ return mac
+}
+
+// Computes flyovermac and aggregates it to existing mac in hopfield
+func aggregateOntoScionMac(t *testing.T, sv []byte, dst addr.IA, l, hin, heg uint16,
+ info path.InfoField, hf *hummingbird.FlyoverHopField, meta hummingbird.MetaHdr) {
+ block, err := aes.NewCipher(sv)
+ require.NoError(t, err)
+ ingress, egress := hin, heg
+
+ akBuffer := make([]byte, hummingbird.AkBufferSize)
+ macBuffer := make([]byte, hummingbird.FlyoverMacBufferSize)
+ xkBuffer := make([]uint32, hummingbird.XkBufferSize)
+
+ ak := hummingbird.DeriveAuthKey(block, hf.ResID, hf.Bw, ingress, egress,
+ meta.BaseTS-uint32(hf.ResStartTime), hf.Duration, akBuffer)
+ flyoverMac := hummingbird.FullFlyoverMac(ak, dst, l, hf.ResStartTime,
+ meta.HighResTS, macBuffer, xkBuffer)
+
+ for i := range hf.HopField.Mac {
+ hf.HopField.Mac[i] ^= flyoverMac[i]
+ }
+}
diff --git a/router/dataplane_test.go b/router/dataplane_test.go
index 590653438e..b5169dca00 100644
--- a/router/dataplane_test.go
+++ b/router/dataplane_test.go
@@ -19,6 +19,7 @@ package router_test
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"net"
"net/netip"
@@ -41,6 +42,7 @@ import (
"github.com/scionproto/scion/pkg/slayers/path"
"github.com/scionproto/scion/pkg/slayers/path/empty"
"github.com/scionproto/scion/pkg/slayers/path/epic"
+ "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
"github.com/scionproto/scion/pkg/slayers/path/onehop"
"github.com/scionproto/scion/pkg/slayers/path/scion"
"github.com/scionproto/scion/private/topology"
@@ -105,6 +107,8 @@ func TestDataPlaneSetKey(t *testing.T) {
})
}
+// deleteme test SetHbirdKey
+
func TestDataPlaneAddExternalInterface(t *testing.T) {
l := control.LinkEnd{
IA: addr.MustParseIA("1-ff00:0:1"),
@@ -420,6 +424,7 @@ func TestDataPlaneRun(t *testing.T) {
assert.NoError(t, ret.SetIA(local))
assert.NoError(t, ret.SetKey(key))
+ assert.NoError(t, ret.SetHbirdKey(key))
return ret
},
},
@@ -454,6 +459,7 @@ func TestDataPlaneRun(t *testing.T) {
return buffer.Bytes()
}
assert.NoError(t, ret.SetKey([]byte("randomkeyformacs")))
+ assert.NoError(t, ret.SetHbirdKey([]byte("randomkeyformacs")))
// We don't care what happens on the internal connection. Sink it.
mInternal := mock_router.NewMockBatchConn(ctrl)
@@ -605,6 +611,7 @@ func TestDataPlaneRun(t *testing.T) {
mInternal.EXPECT().ReadBatch(gomock.Any()).Return(0, nil).AnyTimes()
assert.NoError(t, ret.SetKey([]byte("randomkeyformacs")))
+ assert.NoError(t, ret.SetHbirdKey([]byte("randomkeyformacs")))
// Let the same connection be used for internal and sibling. We only send on the
// latter and we don't care what we receive or where.
ret.SetConnOpener("udpip", router.MockConnOpener{Ctrl: ctrl, Conn: mInternal})
@@ -678,6 +685,7 @@ func TestDataPlaneRun(t *testing.T) {
}
assert.NoError(t, ret.SetKey([]byte("randomkeyformacs")))
+ assert.NoError(t, ret.SetHbirdKey([]byte("randomkeyformacs")))
ret.SetConnOpener("udpip", router.MockConnOpener{Ctrl: ctrl, Conn: mInternal})
assert.NoError(t, ret.AddInternalInterface(addr.Host{}, "udpip", "127.0.0.1:0"))
ret.SetConnOpener("udpip", router.MockConnOpener{Ctrl: ctrl, Conn: mExternal})
@@ -773,6 +781,7 @@ func TestDataPlaneRun(t *testing.T) {
}
assert.NoError(t, ret.SetKey([]byte("randomkeyformacs")))
+ assert.NoError(t, ret.SetHbirdKey([]byte("randomkeyformacs")))
ret.SetConnOpener("udpip", router.MockConnOpener{Ctrl: ctrl, Conn: mInternal})
assert.NoError(t, ret.AddInternalInterface(addr.Host{}, "udpip", "127.0.0.1:0"))
ret.SetConnOpener("udpip", router.MockConnOpener{Ctrl: ctrl, Conn: mExternal})
@@ -1740,14 +1749,47 @@ func TestProcessPkt(t *testing.T) {
}
}
-func assertPktEqual(t *testing.T, a, b *router.Packet) {
+func assertPktEqual(t *testing.T, expected, actual *router.Packet) {
// router.Packet.RemoteAddr is declared as unsafe.Pointer, so it can only be compared
// by address. That isn't what we want. We want the actual addresses compared. We know that
// those addresses are net.UDPAddress because we put them there. So, compare them separately.
- assert.Equal(t, (*net.UDPAddr)(a.RemoteAddr), (*net.UDPAddr)(b.RemoteAddr))
- a.RemoteAddr = nil
- b.RemoteAddr = nil
- assert.Equal(t, a, b)
+ assert.Equal(t, (*net.UDPAddr)(expected.RemoteAddr), (*net.UDPAddr)(actual.RemoteAddr))
+ expected.RemoteAddr = nil
+ actual.RemoteAddr = nil
+ if !assert.Equal(t, expected, actual) && !bytes.Equal(expected.RawPacket, actual.RawPacket) {
+ p := router.PathFromRawPacket(expected.RawPacket)
+ p = toDecoded(t, p)
+ b, err := json.MarshalIndent(p, "", " ")
+ require.NoError(t, err)
+ expectedPktDescription := string(b)
+
+ p = router.PathFromRawPacket(actual.RawPacket)
+ p = toDecoded(t, p)
+ b, err = json.MarshalIndent(p, "", " ")
+ require.NoError(t, err)
+ actualPktDescription := string(b)
+ require.JSONEq(t, expectedPktDescription, actualPktDescription,
+ "JSON description of the Packets.")
+ }
+}
+
+func toDecoded(t *testing.T, p path.Path) path.Path {
+ switch p.Type() {
+ case scion.PathType:
+ scionRaw, ok := p.(*scion.Raw)
+ require.True(t, ok)
+ dec, err := scionRaw.ToDecoded()
+ require.NoError(t, err)
+ return dec
+ case hummingbird.PathType:
+ hbirdRaw, ok := p.(*hummingbird.Raw)
+ require.True(t, ok)
+ dec, err := hbirdRaw.ToDecoded()
+ require.NoError(t, err)
+ return dec
+ default:
+ return p
+ }
}
func toBytes(t *testing.T, spkt *slayers.SCION, dpath path.Path) []byte {
diff --git a/router/export_test.go b/router/export_test.go
index 7062f33f49..c5306b759a 100644
--- a/router/export_test.go
+++ b/router/export_test.go
@@ -18,6 +18,7 @@ package router
import (
"fmt"
+ "math"
"net"
"net/netip"
"unsafe"
@@ -26,6 +27,8 @@ import (
"github.com/scionproto/scion/pkg/addr"
"github.com/scionproto/scion/pkg/private/ptr"
+ "github.com/scionproto/scion/pkg/slayers"
+ "github.com/scionproto/scion/pkg/slayers/path"
"github.com/scionproto/scion/private/topology"
"github.com/scionproto/scion/private/underlay/conn"
"github.com/scionproto/scion/router/bfd"
@@ -85,6 +88,19 @@ func NewPacket(raw []byte, src, dst *net.UDPAddr, ingress, egress uint16) *Packe
return &p
}
+func PathFromRawPacket(raw []byte) path.Path {
+ scionLayer := &slayers.SCION{}
+
+ lastLayer, err := decodeLayers(raw, scionLayer)
+ if err != nil {
+ panic(err) // deleteme
+ }
+ if lastLayer != scionLayer {
+ panic(fmt.Errorf("scion parsing failed")) // deleteme
+ }
+ return scionLayer.Path
+}
+
// MockConnOpener implements the udpip ConnOpener interface with a method that returns a mock
// connection for testing purposes. An instance of this ConnOpener can be installed in a dataplane
// by way of the SetConnOpener method, exported here by the Dataplane type, or by way of
@@ -126,7 +142,8 @@ func mustMakeDP(
internalNextHops map[uint16]netip.AddrPort,
local addr.IA,
neighbors map[uint16]addr.IA,
- key []byte) (dp dataPlane) {
+ key []byte,
+ hbirdKey []byte) (dp dataPlane) {
dp = makeDataPlane(RunConfig{NumProcessors: 1, BatchSize: 64}, false)
@@ -202,6 +219,9 @@ func mustMakeDP(
if err := dp.SetKey(key); err != nil {
panic(err)
}
+ if err := dp.SetHbirdKey(hbirdKey); err != nil {
+ panic(err)
+ }
// The rest is normally done by Run(); it is up to the invoking test:
// add packet pool
@@ -224,7 +244,7 @@ func newDP(
neighbors map[uint16]addr.IA,
key []byte) *dataPlane {
- dp := mustMakeDP(external, linkTypes, connOpener, internalNextHops, local, neighbors, key)
+ dp := mustMakeDP(external, linkTypes, connOpener, internalNextHops, local, neighbors, key, key)
return &dp
}
@@ -246,7 +266,23 @@ func NewDP(
key []byte) *DataPlane {
return &DataPlane{
- mustMakeDP(external, linkTypes, connOpener, internalNextHops, local, neighbors, key),
+ *newDP(external, linkTypes, connOpener, internalNextHops, local, neighbors, key),
+ }
+}
+
+func NewDPWithHummingbirdKey(
+ external []uint16,
+ linkTypes map[uint16]topology.LinkType,
+ connOpener any, // Some implementation of BatchConnOpener, or nil for the default.
+ internalNextHops map[uint16]netip.AddrPort,
+ local addr.IA,
+ neighbors map[uint16]addr.IA,
+ key []byte,
+ hbirdKey []byte) *DataPlane {
+
+ return &DataPlane{
+ mustMakeDP(external, linkTypes, connOpener, internalNextHops, local, neighbors,
+ key, hbirdKey),
}
}
@@ -278,6 +314,10 @@ func ExtractServices(s *Services[netip.AddrPort]) map[addr.SVC][]netip.AddrPort
return s.m
}
+func ExtractInterfaces(dp *DataPlane) [math.MaxUint16 + 1]Link {
+ return dp.dataPlane.interfaces
+}
+
// We cannot know which tests are going to mock which underlay and what the opener's
// signature is. So we'll let the underlay implementation type-assert it.
func (d *DataPlane) SetConnOpener(underlay string, opener any) {
diff --git a/router/tokenbucket/BUILD.bazel b/router/tokenbucket/BUILD.bazel
new file mode 100644
index 0000000000..e8d2faccc2
--- /dev/null
+++ b/router/tokenbucket/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@rules_go//go:def.bzl", "go_library")
+load("//tools:go.bzl", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["tokenbucket.go"],
+ importpath = "github.com/scionproto/scion/router/tokenbucket",
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = ["tokenbucket_test.go"],
+ deps = [
+ ":go_default_library",
+ "@com_github_stretchr_testify//assert:go_default_library",
+ ],
+)
diff --git a/router/tokenbucket/tokenbucket.go b/router/tokenbucket/tokenbucket.go
new file mode 100644
index 0000000000..aa9f45198a
--- /dev/null
+++ b/router/tokenbucket/tokenbucket.go
@@ -0,0 +1,73 @@
+package tokenbucket
+
+import (
+ "sync"
+ "time"
+)
+
+type TokenBucket struct {
+ CurrentTokens float64
+ LastTimeApplied time.Time
+
+ //Burst Size
+ CBS float64
+
+ //In bytes per second
+ CIR float64
+
+ // Lock
+ lock sync.Mutex
+}
+
+// Initializes a new tockenbucket for the given burstSize and rate
+func NewTokenBucket(initialTime time.Time, burstSize float64, rate float64) *TokenBucket {
+ return &TokenBucket{
+ CurrentTokens: rate,
+ CIR: rate,
+ CBS: burstSize,
+ LastTimeApplied: initialTime,
+ }
+}
+
+// Sets a new rate for the token bucket
+func (t *TokenBucket) SetRate(rate float64) {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+ t.CIR = rate
+}
+
+// Sets a new burst size for the token bucket
+func (t *TokenBucket) SetBurstSize(burstSize float64) {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+ t.CBS = burstSize
+}
+
+// Apply calculates the current available tokens and checks whether there
+// are enough tokens available. The success is indicated by a bool.
+func (t *TokenBucket) Apply(size int, now time.Time) bool {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+ // Increase available tokens according to time passed since last call
+ // Apply() is expected to be called from different threads
+ // As a consequence, it is possible for now to be older than LastTimeApplied
+ if !now.Before(t.LastTimeApplied) {
+ t.CurrentTokens += now.Sub(t.LastTimeApplied).Seconds() * t.CIR
+ t.CurrentTokens = min(t.CurrentTokens, t.CBS)
+ t.LastTimeApplied = now
+ }
+ if t.CurrentTokens >= float64(size) {
+ t.CurrentTokens -= float64(size)
+ return true
+ }
+ return false
+}
+
+// This function calculates the minimal value of two float64.
+func min(a float64, b float64) float64 {
+ if a > b {
+ return b
+ } else {
+ return a
+ }
+}
diff --git a/router/tokenbucket/tokenbucket_test.go b/router/tokenbucket/tokenbucket_test.go
new file mode 100644
index 0000000000..5a545d27c0
--- /dev/null
+++ b/router/tokenbucket/tokenbucket_test.go
@@ -0,0 +1,143 @@
+// Copyright 2025 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tokenbucket_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/scionproto/scion/router/tokenbucket"
+)
+
+type entry struct {
+ length int
+ arrivalTime time.Time
+ result bool
+}
+type test struct {
+ name string
+ entries []entry
+ bucket *tokenbucket.TokenBucket
+}
+
+// TestTokenBucketAlgorithm checks that the token bucket implementation detects if a given
+// amount of bytes exceed the allowance or not.
+func TestTokenBucketAlgorithm(t *testing.T) {
+
+ var startTime = time.Unix(0, 0)
+
+ tests := []test{
+ {
+ name: "TestApplyDoesAllowArrivalBehindTheLastArrival",
+ bucket: tokenbucket.NewTokenBucket(startTime.Add(1), 1024, 1024),
+ entries: []entry{
+ {
+ length: 1,
+ arrivalTime: startTime,
+ result: true,
+ },
+ },
+ },
+ {
+ name: "FullBandwidthCanBeConsumedAtOnce",
+ bucket: tokenbucket.NewTokenBucket(startTime, 1024, 1024),
+ entries: []entry{
+ {
+ length: 1024,
+ arrivalTime: startTime,
+ result: true,
+ },
+ {
+ length: 1,
+ arrivalTime: startTime,
+ result: false,
+ },
+ },
+ },
+ {
+ name: "FullBandwidthCanBeConsumedOverMultiplePackets",
+ bucket: tokenbucket.NewTokenBucket(startTime, 1024, 1024),
+ entries: []entry{
+ {
+ length: 512,
+ arrivalTime: startTime,
+ result: true,
+ },
+ {
+ length: 512,
+ arrivalTime: startTime,
+ result: true,
+ },
+ {
+ length: 1,
+ arrivalTime: startTime,
+ result: false,
+ },
+ },
+ },
+ {
+ name: "CurrentTokensRegenerate",
+ bucket: tokenbucket.NewTokenBucket(startTime, 1024, 1024),
+ entries: []entry{
+ {
+ length: 1024,
+ arrivalTime: startTime,
+ result: true,
+ },
+ {
+ length: 512,
+ arrivalTime: startTime.Add(500 * time.Millisecond),
+ result: true,
+ },
+ {
+ length: 1,
+ arrivalTime: startTime.Add(500 * time.Millisecond),
+ result: false,
+ },
+ },
+ },
+ {
+ name: "CurrentTokensIsLimitedByCBS",
+ bucket: tokenbucket.NewTokenBucket(startTime, 2048, 1024),
+ entries: []entry{
+ {
+ length: 2049,
+ arrivalTime: startTime.Add(1 * time.Second),
+ result: false,
+ },
+ {
+ length: 2048,
+ arrivalTime: startTime.Add(1 * time.Second),
+ result: true,
+ },
+ {
+ length: 1,
+ arrivalTime: startTime.Add(1 * time.Second),
+ result: false,
+ },
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ for _, en := range tc.entries {
+ assert.Equal(t, en.result, tc.bucket.Apply(en.length, en.arrivalTime), tc.name)
+ }
+ })
+ }
+}