oxirush-utils
What the OxiRush crates share, in Rust: the identities of 3GPP networks as
values, and one JSON tree with paths for the messages of every protocol, in
no_std and with no dependency unless a feature asks for one.
use ;
310-410: [13, 00, 14] in NAS, [13, 40, 01] in NGAP
5g-guti-310410caff81deadbeef: [f2, 13, 00, 14, ca, ff, 81, de, ad, be, ef]
Each example is followed by its output.
Installation
[]
= { = "0.1", = ["alloc"] }
# With the tree and its paths, which need an allocator too:
# oxirush-utils = { version = "0.1", features = ["tree"] }
# Without an allocator, the values that have their digits in place:
# oxirush-utils = "0.1"
| Feature | Adds | Through |
|---|---|---|
alloc |
What holds a text or a list: the SUPI that owns its text, the SUCIs, TBCD strings, identity digits as octets | |
tree |
The tree of a message, its forms and its paths | serde_json |
serde |
Serialize and Deserialize for a type through its tree |
serde_core |
idna |
The realm and the username of a NAI checked as internationalized names too: Unicode normalization form C, and a realm that IDNA takes as it is written | idna, unicode-normalization |
No feature is on by default. tree turns on the float_roundtrip feature of
serde_json, so a number with a fraction is read as the nearest f64. A tree
keeps the members of an object in the order the program wrote them.
The minimum supported Rust version is 1.88.
An identity through its forms
A value of identity is checked when it is made, and prints whole, in the
text of the service based interfaces. An MNC has two or three digits, and
how many is part of the value: 208-93 is not 208-093.
use ;
IMSI 310410123456789: PLMN 310-410, MSIN 123456789
suci-0-310-410-0-0-0-123456789: [01, 13, 00, 14, f0, ff, 00, 00, 21, 43, 65, 87, f9]
imsi-310410123456789
5g-s-tmsi-ff81deadbeef
`mnc` has 1 digit, and must have 2 or 3
Where each form goes
The NAS octets of an identity are the value of an IE of oxirush-nas. The AP octets of a PLMN are those of the identities of oxirush-ngap and oxirush-s1ap. The messages of all three are trees of this crate, read by the same paths.
use ;
use ;
use ;
use tree;
7e0042010177000bf2130014caff81deadbeef
PLMNIdentity(310-410): [13, 40, 01]
[Number(3735928559)]
IMEI, IMEISV, and the identities of EPS
use ;
[29, 80, 39, 00, 00, 00, 00, 10]
490154203237510
490154203237518
01
4g-guti-20893000201c00004f4
208-93-000001 208-93-0001
The SUCI in the form of a NAI
A SUPI that is not an IMSI is a NAI, and so is its SUCI. A NAI follows the
grammar of RFC 7542; with the idna feature, its realm must also be a name
that IDNA takes as written.
use ;
NetworkSpecific Some("user17") example.com
0x11
`realm` is written as a domain name of two labels at least, of letters, digits and hyphens, with no hyphen at either end and 63 octets at most each, as example.com
What other implementations send
from_nas_octets takes only the octets that the clause writes. The readers
named _tolerant also take what other implementations are known to send,
and say what departs from the clause.
The documentation
lists each tolerance with its clause and its evidence.
use Imeisv;
`imeisv` has the half octet 0x0 where its end mark is, and the end mark is 0xf
1110000000000000, read with: `imeisv` has the half octet 0x0 where its end mark is, and the end mark is 0xf
A type as a tree
With the tree feature, a message is a tree::Value: a JSON value in one
form for every protocol, with octets in lower-case hexadecimal, numbers as
JSON numbers, names read however they are written, and members in the order
of the specification. tree! writes one as serde_json::json! does, and
keeps that order.
use ;
{"sst":1,"sd":"0000ff"}
Slice { sst: 2, sd: Some([0, 0, 255]) }
this is not a value of S-NSSAI: a number from 0 to 255 is expected, and 256 is written, at sst
Paths
A path is a JSON pointer: the name of a member, the position of an entry, *
for each of them, and - for the end of a list in an insertion.
use tree;
[Number(1), Number(2)]
[]
"sts" is not a member of this value, which has sst, at /requested-nssai/0/sts
/message-type = "registration-request"
/requested-nssai/0/sst = 1
/requested-nssai/1/sst = 2
/requested-nssai/1/sd = "0000ff"
[{"sst":1,"sd":"000001"},{"sst":3}]
Any format of serde
With the serde feature, a type is serialized and deserialized through its
tree, so YAML and JSON carry the same form.
use ;
// `Serialize` and `Deserialize`, through the tree
impl_serde!;
{"seconds":3600}
seconds: 3600
Timer { seconds: 3600 }
Limits
- It knows no codec and no message: the crates of each protocol do.
- It conceals no SUCI and derives no key: oxirush-security does.
Supichecks that a NAI has a realm, and its length, but not its grammar:nai::check_realmandnai::check_usernamecheck that.- A value of
identityholds what the clauses allow. Octets that are no such identity stay octets in the crate that reads them. - It has no JSON parser of its own: serde_json's reads a tree.
Performance
The tree of the crate against a serde_json::Value of the default
features, on the same JSON, at 8865ad4 on core 2 of an AMD Ryzen 7 5700G
(Linux 7.0, Rust 1.98.1), on 2026-10-18. A ratio is the time of serde_json
over this crate's, and "about the same" is within 10 %:
| Operation | This crate | serde_json 1 | serde_json / this crate |
|---|---|---|---|
| Read a tree of 925 octets | 2,645 ns | 2,880 ns | about the same |
| Write it | 923 ns ±15% between builds | 2,140 ns | 2.3x |
| Select a value of it | 219 ns | 238 ns | about the same |
| Put 100,000 members into an object | 21.0 ms | 18.6 ms | 0.89x |
| Read an object of 100,000 members, 1,088,891 octets | 14.6 ms | 20.7 ms | 1.4x |
A tree keeps the members of an object in their order, where a
serde_json::Value sorts them. Reading a PLMN, an IMSI or a 5G-GUTI takes 9
to 14 ns without an allocation, and the SUCI of an IMSI 80 ns.
benches/ has the allocations, the other identities and
the method. In benches/:
Tests and fuzzing
The identities are tested against their clauses, against what Open5GS,
free5GC and PacketRusher sent in lab runs, and against themselves: every
value is read back from the octets it writes, and any octets are either a
value that writes them back or no value. The tests of oxirush-nas and
oxirush-ngap on paths run here too, on trees written out without a codec,
and so do trees of 65,536 entries, within a time bound. fuzz/ has two
cargo-fuzz targets: identity reads
octets and text as each identity, and paths takes any path and any edit on
any tree.
Each Rust block of this page is checked with what it prints
(tests/readme.rs says how). The block that uses other
crates of the family runs in the package of family/, which takes
those crates at the revisions that its manifest names.
A pull request comes with a test for what it changes. Sign off your commits
(git commit -s): by contributing you agree to the Developer Certificate
of Origin. SECURITY.md
says how to report a vulnerability.
The OxiRush family
- oxirush-utils: the 3GPP identities, and the JSON tree with its paths that the others share.
- oxirush-aper: aligned PER, the runtime of the NGAP and S1AP codecs.
- oxirush-nas: the NAS messages of 5GS and EPS.
- oxirush-security: NAS security, AKA and the derivation of keys.
- oxirush-ngap: NGAP, between a gNB and an AMF.
- oxirush-s1ap: S1AP, between an eNB and an MME.
- oxirush-gtp-u: GTP-U tunnels, with a test UPF.
- oxirush-sctp: SCTP sockets, which carry NGAP and S1AP.
License
Copyright 2025-2026 Valentin D'Emmanuele. Licensed under the Apache License, Version 2.0: see LICENSE.