Skip to main content

netget/server/m3ua/
codec.rs

1//! M3UA wire codec (RFC 4666).
2//!
3//! Pure functions over bytes: the common header, the TLV parameter encoding, the Protocol Data
4//! field, and small builders for the messages an SGP sends. Nothing here touches a socket, the
5//! LLM or `AppState`, so every rule below is pinned directly against literal bytes in
6//! `tests/server/m3ua/codec_test.rs`.
7//!
8//! # The two rules that are usually got wrong
9//!
10//! **1. Parameter padding is not counted in the parameter length.** RFC 4666 section 3.2: the
11//! Parameter Length covers Tag + Length + Value and *excludes* the padding that rounds the
12//! parameter up to a 4-octet boundary. A parameter carrying the two octets `"ok"` therefore
13//! declares length 6 and occupies 8 octets on the wire, the last two being zeros nobody counts.
14//! Getting this backwards produces a stream that decodes correctly against itself and against
15//! nothing else, which is why the codec tests assert octets rather than round-trips.
16//!
17//! **2. The Message Length in the common header is the whole message including the header.**
18//! NetGet *includes* the parameters' padding octets in it, which is what the reference SIGTRAN
19//! stacks do. A peer that excludes it produces a length that is not a multiple of four; the
20//! reader in `mod.rs` reconciles that by consuming the alignment octets that must be on the
21//! wire regardless (see `alignment_slack`).
22
23use std::fmt;
24
25/// M3UA protocol version (RFC 4666 section 3.1.1). Only version 1 exists.
26pub const VERSION: u8 = 1;
27
28/// Common header length: version, reserved, class, type, 32-bit length.
29pub const HEADER_LEN: usize = 8;
30
31/// Refuse to allocate for anything larger. M3UA has no stated ceiling, but an SS7 MSU is at
32/// most 272 octets of user part, so this is generous by three orders of magnitude and still
33/// bounds what an unauthenticated peer can make the server reserve.
34pub const MAX_MESSAGE_LEN: usize = 65_535;
35
36/// Largest SS7 user part this server will put inside a Protocol Data parameter, in octets.
37///
38/// `MAX_MESSAGE_LEN` bounds the **decode** side, where a hostile peer chooses the number.
39/// This bounds the **encode** side, where the model does — and that side had no bound at
40/// all: `Parameter::write_into` writes `declared_len()` as a `u16`, so a user part of 65520
41/// octets or more wrapped the Parameter Length field and produced a message no SS7 peer
42/// could parse.
43///
44/// The arithmetic, and **the padding rule is part of it**. A body is always a whole number of
45/// 4-octet words — every parameter is padded up to one — so the largest body that fits under
46/// [`MAX_MESSAGE_LEN`] is `((65535 − 8) / 4) * 4 = 65524`, not 65527. Take off the 4-octet
47/// parameter header and Protocol Data's 12 fixed octets (OPC, DPC, SI, NI, MP, SLS) and 65508
48/// is left. The earlier form of this constant subtracted the headers and forgot the padding,
49/// which put it three octets too high — so a payload at exactly the documented limit produced
50/// a 65536-octet message, one past the ceiling. That is the same "padding is not in the
51/// length" trap this module's header warns about, met from the other side.
52///
53/// This is three orders of magnitude above anything real. A genuine MSU carries at most 272
54/// octets of user part, which is why exceeding it means the model has misunderstood the
55/// field rather than hit a legitimate ceiling. It is also a *per-parameter* guide: a DATA
56/// message that also carries Network Appearance, Routing Context and Correlation ID has 24
57/// fewer octets to spend, and [`Message::encode`] is what enforces the real ceiling.
58pub const MAX_USER_DATA_LEN: usize = ((MAX_MESSAGE_LEN - HEADER_LEN) / 4) * 4 - 4 - 12;
59
60// ---------------------------------------------------------------------------
61// Message classes and types (RFC 4666 section 3.1.3 / 3.1.4)
62// ---------------------------------------------------------------------------
63
64pub const CLASS_MGMT: u8 = 0;
65pub const CLASS_TRANSFER: u8 = 1;
66pub const CLASS_SSNM: u8 = 2;
67pub const CLASS_ASPSM: u8 = 3;
68pub const CLASS_ASPTM: u8 = 4;
69pub const CLASS_RKM: u8 = 9;
70
71// MGMT (class 0)
72pub const MGMT_ERR: u8 = 0;
73pub const MGMT_NTFY: u8 = 1;
74
75// Transfer (class 1)
76pub const TRANSFER_DATA: u8 = 1;
77
78// SSNM (class 2)
79pub const SSNM_DUNA: u8 = 1;
80pub const SSNM_DAVA: u8 = 2;
81pub const SSNM_DAUD: u8 = 3;
82pub const SSNM_SCON: u8 = 4;
83pub const SSNM_DUPU: u8 = 5;
84pub const SSNM_DRST: u8 = 6;
85
86// ASPSM (class 3)
87pub const ASPSM_ASPUP: u8 = 1;
88pub const ASPSM_ASPDN: u8 = 2;
89pub const ASPSM_BEAT: u8 = 3;
90pub const ASPSM_ASPUP_ACK: u8 = 4;
91pub const ASPSM_ASPDN_ACK: u8 = 5;
92pub const ASPSM_BEAT_ACK: u8 = 6;
93
94// ASPTM (class 4)
95pub const ASPTM_ASPAC: u8 = 1;
96pub const ASPTM_ASPIA: u8 = 2;
97pub const ASPTM_ASPAC_ACK: u8 = 3;
98pub const ASPTM_ASPIA_ACK: u8 = 4;
99
100// RKM (class 9)
101pub const RKM_REG_REQ: u8 = 1;
102pub const RKM_REG_RSP: u8 = 2;
103pub const RKM_DEREG_REQ: u8 = 3;
104pub const RKM_DEREG_RSP: u8 = 4;
105
106// ---------------------------------------------------------------------------
107// Parameter tags (RFC 4666 section 3.2)
108// ---------------------------------------------------------------------------
109
110/// Common parameters, shared with the other SIGTRAN adaptation layers.
111pub const TAG_INFO_STRING: u16 = 0x0004;
112pub const TAG_ROUTING_CONTEXT: u16 = 0x0006;
113pub const TAG_DIAGNOSTIC_INFO: u16 = 0x0007;
114pub const TAG_HEARTBEAT_DATA: u16 = 0x0009;
115pub const TAG_TRAFFIC_MODE_TYPE: u16 = 0x000b;
116pub const TAG_ERROR_CODE: u16 = 0x000c;
117pub const TAG_STATUS: u16 = 0x000d;
118pub const TAG_ASP_IDENTIFIER: u16 = 0x0011;
119pub const TAG_AFFECTED_POINT_CODE: u16 = 0x0012;
120pub const TAG_CORRELATION_ID: u16 = 0x0013;
121
122/// M3UA-specific parameters.
123pub const TAG_NETWORK_APPEARANCE: u16 = 0x0200;
124pub const TAG_USER_CAUSE: u16 = 0x0204;
125pub const TAG_CONGESTION_INDICATIONS: u16 = 0x0205;
126pub const TAG_CONCERNED_DESTINATION: u16 = 0x0206;
127pub const TAG_ROUTING_KEY: u16 = 0x0207;
128pub const TAG_REGISTRATION_RESULT: u16 = 0x0208;
129pub const TAG_DEREGISTRATION_RESULT: u16 = 0x0209;
130pub const TAG_LOCAL_ROUTING_KEY_ID: u16 = 0x020a;
131pub const TAG_DESTINATION_POINT_CODE: u16 = 0x020b;
132pub const TAG_SERVICE_INDICATORS: u16 = 0x020c;
133pub const TAG_ORIGINATING_POINT_CODE_LIST: u16 = 0x020e;
134pub const TAG_PROTOCOL_DATA: u16 = 0x0210;
135pub const TAG_REGISTRATION_STATUS: u16 = 0x0212;
136pub const TAG_DEREGISTRATION_STATUS: u16 = 0x0213;
137
138// ---------------------------------------------------------------------------
139// Error codes (RFC 4666 section 3.8.1), carried as a 32-bit Error Code parameter
140// ---------------------------------------------------------------------------
141
142pub const ERR_INVALID_VERSION: u32 = 0x01;
143pub const ERR_UNSUPPORTED_MESSAGE_CLASS: u32 = 0x03;
144pub const ERR_UNSUPPORTED_MESSAGE_TYPE: u32 = 0x04;
145pub const ERR_UNSUPPORTED_TRAFFIC_MODE: u32 = 0x05;
146pub const ERR_UNEXPECTED_MESSAGE: u32 = 0x06;
147pub const ERR_PROTOCOL_ERROR: u32 = 0x07;
148pub const ERR_INVALID_STREAM_IDENTIFIER: u32 = 0x09;
149pub const ERR_REFUSED_MANAGEMENT_BLOCKING: u32 = 0x0d;
150pub const ERR_ASP_IDENTIFIER_REQUIRED: u32 = 0x0e;
151pub const ERR_INVALID_ASP_IDENTIFIER: u32 = 0x0f;
152pub const ERR_INVALID_PARAMETER_VALUE: u32 = 0x11;
153pub const ERR_PARAMETER_FIELD_ERROR: u32 = 0x12;
154pub const ERR_UNEXPECTED_PARAMETER: u32 = 0x13;
155pub const ERR_DESTINATION_STATUS_UNKNOWN: u32 = 0x14;
156pub const ERR_INVALID_NETWORK_APPEARANCE: u32 = 0x15;
157pub const ERR_MISSING_PARAMETER: u32 = 0x16;
158pub const ERR_INVALID_ROUTING_CONTEXT: u32 = 0x19;
159pub const ERR_NO_CONFIGURED_AS_FOR_ASP: u32 = 0x1a;
160
161/// Traffic Mode Type values (RFC 4666 section 3.6.1).
162pub const TRAFFIC_MODE_OVERRIDE: u32 = 1;
163pub const TRAFFIC_MODE_LOADSHARE: u32 = 2;
164pub const TRAFFIC_MODE_BROADCAST: u32 = 3;
165
166/// Status Type values for NTFY (RFC 4666 section 3.8.2).
167pub const STATUS_TYPE_AS_STATE_CHANGE: u16 = 1;
168pub const STATUS_TYPE_OTHER: u16 = 2;
169
170/// Status Information under Status Type 1.
171pub const STATUS_AS_INACTIVE: u16 = 2;
172pub const STATUS_AS_ACTIVE: u16 = 3;
173pub const STATUS_AS_PENDING: u16 = 4;
174
175/// Status Information under Status Type 2.
176pub const STATUS_INSUFFICIENT_ASP_RESOURCES: u16 = 1;
177pub const STATUS_ALTERNATE_ASP_ACTIVE: u16 = 2;
178pub const STATUS_ASP_FAILURE: u16 = 3;
179
180/// Human name for a message class, for logs and event data.
181pub fn class_name(class: u8) -> &'static str {
182    match class {
183        CLASS_MGMT => "MGMT",
184        CLASS_TRANSFER => "Transfer",
185        CLASS_SSNM => "SSNM",
186        CLASS_ASPSM => "ASPSM",
187        CLASS_ASPTM => "ASPTM",
188        5 => "RKM(reserved)",
189        6 => "IIM",
190        CLASS_RKM => "RKM",
191        _ => "unknown",
192    }
193}
194
195/// Human name for a (class, type) pair, for logs and event data.
196pub fn message_name(class: u8, msg_type: u8) -> &'static str {
197    match (class, msg_type) {
198        (CLASS_MGMT, MGMT_ERR) => "ERR",
199        (CLASS_MGMT, MGMT_NTFY) => "NTFY",
200        (CLASS_TRANSFER, TRANSFER_DATA) => "DATA",
201        (CLASS_SSNM, SSNM_DUNA) => "DUNA",
202        (CLASS_SSNM, SSNM_DAVA) => "DAVA",
203        (CLASS_SSNM, SSNM_DAUD) => "DAUD",
204        (CLASS_SSNM, SSNM_SCON) => "SCON",
205        (CLASS_SSNM, SSNM_DUPU) => "DUPU",
206        (CLASS_SSNM, SSNM_DRST) => "DRST",
207        (CLASS_ASPSM, ASPSM_ASPUP) => "ASPUP",
208        (CLASS_ASPSM, ASPSM_ASPDN) => "ASPDN",
209        (CLASS_ASPSM, ASPSM_BEAT) => "BEAT",
210        (CLASS_ASPSM, ASPSM_ASPUP_ACK) => "ASPUP ACK",
211        (CLASS_ASPSM, ASPSM_ASPDN_ACK) => "ASPDN ACK",
212        (CLASS_ASPSM, ASPSM_BEAT_ACK) => "BEAT ACK",
213        (CLASS_ASPTM, ASPTM_ASPAC) => "ASPAC",
214        (CLASS_ASPTM, ASPTM_ASPIA) => "ASPIA",
215        (CLASS_ASPTM, ASPTM_ASPAC_ACK) => "ASPAC ACK",
216        (CLASS_ASPTM, ASPTM_ASPIA_ACK) => "ASPIA ACK",
217        (CLASS_RKM, RKM_REG_REQ) => "REG REQ",
218        (CLASS_RKM, RKM_REG_RSP) => "REG RSP",
219        (CLASS_RKM, RKM_DEREG_REQ) => "DEREG REQ",
220        (CLASS_RKM, RKM_DEREG_RSP) => "DEREG RSP",
221        _ => "unknown",
222    }
223}
224
225/// MTP3 Service Indicator names (ITU-T Q.704 table 1), for the DATA event.
226///
227/// Purely descriptive: the model reasons far better about `"si_name": "ISUP"` than about
228/// `"si": 5`, and both are supplied so nothing has to be guessed from the name.
229pub fn si_name(si: u8) -> &'static str {
230    match si {
231        0 => "SNM",
232        1 => "MTP testing",
233        2 => "special MTP testing",
234        3 => "SCCP",
235        4 => "TUP",
236        5 => "ISUP",
237        6 => "DUP (call and circuit related)",
238        7 => "DUP (facility registration/cancellation)",
239        8 => "MTP testing user part",
240        9 => "B-ISUP",
241        10 => "SAT-ISUP",
242        _ => "unassigned",
243    }
244}
245
246/// Name of an error code, for logs and for the `m3ua_error_received` event.
247pub fn error_code_name(code: u32) -> &'static str {
248    match code {
249        ERR_INVALID_VERSION => "Invalid Version",
250        ERR_UNSUPPORTED_MESSAGE_CLASS => "Unsupported Message Class",
251        ERR_UNSUPPORTED_MESSAGE_TYPE => "Unsupported Message Type",
252        ERR_UNSUPPORTED_TRAFFIC_MODE => "Unsupported Traffic Mode Type",
253        ERR_UNEXPECTED_MESSAGE => "Unexpected Message",
254        ERR_PROTOCOL_ERROR => "Protocol Error",
255        ERR_INVALID_STREAM_IDENTIFIER => "Invalid Stream Identifier",
256        ERR_REFUSED_MANAGEMENT_BLOCKING => "Refused - Management Blocking",
257        ERR_ASP_IDENTIFIER_REQUIRED => "ASP Identifier Required",
258        ERR_INVALID_ASP_IDENTIFIER => "Invalid ASP Identifier",
259        ERR_INVALID_PARAMETER_VALUE => "Invalid Parameter Value",
260        ERR_PARAMETER_FIELD_ERROR => "Parameter Field Error",
261        ERR_UNEXPECTED_PARAMETER => "Unexpected Parameter",
262        ERR_DESTINATION_STATUS_UNKNOWN => "Destination Status Unknown",
263        ERR_INVALID_NETWORK_APPEARANCE => "Invalid Network Appearance",
264        ERR_MISSING_PARAMETER => "Missing Parameter",
265        ERR_INVALID_ROUTING_CONTEXT => "Invalid Routing Context",
266        ERR_NO_CONFIGURED_AS_FOR_ASP => "No Configured AS for ASP",
267        _ => "unassigned",
268    }
269}
270
271// ---------------------------------------------------------------------------
272// Errors
273// ---------------------------------------------------------------------------
274
275/// A decode failure, carrying the M3UA error code the peer earns for it.
276///
277/// `reason` is for the log only. It never reaches the peer: RFC 4666's ERR message carries a
278/// numeric Error Code, and the optional Diagnostic Information parameter is left empty rather
279/// than filled with an internal string.
280#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct WireError {
282    pub error_code: u32,
283    pub reason: String,
284}
285
286impl WireError {
287    fn new(error_code: u32, reason: impl Into<String>) -> Self {
288        Self {
289            error_code,
290            reason: reason.into(),
291        }
292    }
293}
294
295impl fmt::Display for WireError {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        write!(
298            f,
299            "{} (error code 0x{:02x} {})",
300            self.reason,
301            self.error_code,
302            error_code_name(self.error_code)
303        )
304    }
305}
306
307impl std::error::Error for WireError {}
308
309// ---------------------------------------------------------------------------
310// Common header
311// ---------------------------------------------------------------------------
312
313/// The 8-octet M3UA common header.
314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315pub struct CommonHeader {
316    pub version: u8,
317    pub reserved: u8,
318    pub class: u8,
319    pub msg_type: u8,
320    /// Total message length in octets, *including* these 8.
321    pub length: u32,
322}
323
324/// Parse and validate the common header.
325///
326/// Everything is checked before the caller allocates the body, so a peer cannot choose the
327/// size of a buffer and `length - HEADER_LEN` cannot underflow.
328pub fn parse_header(bytes: &[u8]) -> Result<CommonHeader, WireError> {
329    if bytes.len() < HEADER_LEN {
330        return Err(WireError::new(
331            ERR_PROTOCOL_ERROR,
332            format!("common header truncated at {} octets", bytes.len()),
333        ));
334    }
335    let version = bytes[0];
336    if version != VERSION {
337        return Err(WireError::new(
338            ERR_INVALID_VERSION,
339            format!("M3UA version {version} is not 1"),
340        ));
341    }
342    let length = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
343    if (length as usize) < HEADER_LEN {
344        return Err(WireError::new(
345            ERR_PROTOCOL_ERROR,
346            format!("message length {length} is shorter than the common header"),
347        ));
348    }
349    if length as usize > MAX_MESSAGE_LEN {
350        return Err(WireError::new(
351            ERR_PROTOCOL_ERROR,
352            format!("message length {length} exceeds the {MAX_MESSAGE_LEN}-octet ceiling"),
353        ));
354    }
355    Ok(CommonHeader {
356        version,
357        reserved: bytes[1],
358        class: bytes[2],
359        msg_type: bytes[3],
360        length,
361    })
362}
363
364/// Octets of 4-byte alignment slack a declared message length implies.
365///
366/// NetGet includes parameter padding in the Message Length, so its own messages always yield
367/// zero here. A peer that excludes the final parameter's padding declares a length that is not
368/// a multiple of four while still writing the padding octets, per RFC 4666 section 3.2 ("the
369/// sender pads the parameter"). The reader consumes this many octets after the declared body so
370/// the next header starts where it should.
371pub fn alignment_slack(length: u32) -> usize {
372    padding_for(length as usize)
373}
374
375/// Octets needed to round `len` up to a 4-octet boundary.
376pub fn padding_for(len: usize) -> usize {
377    (4 - (len % 4)) % 4
378}
379
380// ---------------------------------------------------------------------------
381// Parameters
382// ---------------------------------------------------------------------------
383
384/// One TLV parameter, without its padding — the padding is a property of the encoding, never
385/// of the value, and folding it into `value` is how a decoder starts handing 3 stray zero
386/// octets to a user part.
387#[derive(Clone, Debug, PartialEq, Eq)]
388pub struct Parameter {
389    pub tag: u16,
390    pub value: Vec<u8>,
391}
392
393impl Parameter {
394    pub fn new(tag: u16, value: impl Into<Vec<u8>>) -> Self {
395        Self {
396            tag,
397            value: value.into(),
398        }
399    }
400
401    /// A parameter whose whole value is one 32-bit field (routing context, error code, …).
402    pub fn u32(tag: u16, value: u32) -> Self {
403        Self::new(tag, value.to_be_bytes().to_vec())
404    }
405
406    /// The value of the Parameter Length field: 4 header octets plus the value, and **not**
407    /// the padding (RFC 4666 section 3.2).
408    pub fn declared_len(&self) -> usize {
409        4 + self.value.len()
410    }
411
412    /// Octets this parameter occupies on the wire, padding included.
413    pub fn wire_len(&self) -> usize {
414        let declared = self.declared_len();
415        declared + padding_for(declared)
416    }
417
418    /// Append the TLV, refusing a value whose Parameter Length would not fit the 16-bit field.
419    ///
420    /// The check comes before anything is written, so a refusal leaves `out` untouched: a
421    /// partial TLV would corrupt every parameter after it just as surely as the wrong length
422    /// did. The old `declared as u16` was worse than an oversize parameter — a 65532-octet
423    /// value makes `declared` 65536, which narrows to **0**, below the 4-octet TLV header
424    /// itself, so the parameter became unparseable and took every later parameter with it.
425    /// Silent in release and silent in debug, because a cast does not overflow-check.
426    pub fn write_into(&self, out: &mut Vec<u8>) -> Result<(), WireError> {
427        let declared = self.declared_len();
428        if declared > u16::MAX as usize {
429            return Err(WireError::new(
430                ERR_PROTOCOL_ERROR,
431                format!(
432                    "parameter 0x{:04x} declares {declared} octets, past the {} a 16-bit \
433                     Parameter Length can carry",
434                    self.tag,
435                    u16::MAX
436                ),
437            ));
438        }
439        out.extend_from_slice(&self.tag.to_be_bytes());
440        out.extend_from_slice(&(declared as u16).to_be_bytes());
441        out.extend_from_slice(&self.value);
442        out.resize(out.len() + padding_for(declared), 0);
443        Ok(())
444    }
445
446    /// The value read as a 32-bit field, or `None` if it is not exactly four octets.
447    pub fn as_u32(&self) -> Option<u32> {
448        let bytes: [u8; 4] = self.value.as_slice().try_into().ok()?;
449        Some(u32::from_be_bytes(bytes))
450    }
451}
452
453/// Parse a message body into its parameters.
454///
455/// Tolerates a final parameter whose padding octets were not written, which is what a sender
456/// that excludes padding from the Message Length produces at the end of a message.
457pub fn parse_parameters(body: &[u8]) -> Result<Vec<Parameter>, WireError> {
458    let mut out = Vec::new();
459    let mut offset = 0usize;
460    while offset < body.len() {
461        let remaining = body.len() - offset;
462        if remaining < 4 {
463            // Fewer than four octets left. Trailing zeros are the previous parameter's padding;
464            // anything else is a truncated parameter header.
465            if body[offset..].iter().all(|b| *b == 0) {
466                break;
467            }
468            return Err(WireError::new(
469                ERR_PARAMETER_FIELD_ERROR,
470                format!("{remaining} trailing octets are too short for a parameter header"),
471            ));
472        }
473        let tag = u16::from_be_bytes([body[offset], body[offset + 1]]);
474        let declared = u16::from_be_bytes([body[offset + 2], body[offset + 3]]) as usize;
475        if declared < 4 {
476            return Err(WireError::new(
477                ERR_PARAMETER_FIELD_ERROR,
478                format!("parameter 0x{tag:04x} declares length {declared}, below the 4-octet TLV header"),
479            ));
480        }
481        let end = offset + declared;
482        if end > body.len() {
483            return Err(WireError::new(
484                ERR_PARAMETER_FIELD_ERROR,
485                format!(
486                    "parameter 0x{tag:04x} declares {declared} octets but only {remaining} remain"
487                ),
488            ));
489        }
490        out.push(Parameter {
491            tag,
492            value: body[offset + 4..end].to_vec(),
493        });
494        // Padding is not in `declared`, so skip it explicitly; a final parameter may have had
495        // its padding omitted, in which case we simply land on the end of the body.
496        offset = (end + padding_for(declared)).min(body.len());
497    }
498    Ok(out)
499}
500
501// ---------------------------------------------------------------------------
502// Messages
503// ---------------------------------------------------------------------------
504
505/// A whole M3UA message: class, type and its parameters in wire order.
506#[derive(Clone, Debug, PartialEq, Eq)]
507pub struct Message {
508    pub class: u8,
509    pub msg_type: u8,
510    pub parameters: Vec<Parameter>,
511}
512
513impl Message {
514    pub fn new(class: u8, msg_type: u8) -> Self {
515        Self {
516            class,
517            msg_type,
518            parameters: Vec::new(),
519        }
520    }
521
522    pub fn with(mut self, parameter: Parameter) -> Self {
523        self.parameters.push(parameter);
524        self
525    }
526
527    /// Append `parameter` only when `value` is present. Every optional parameter in this file
528    /// goes through here, so "omitted" and "present but zero" stay distinguishable.
529    pub fn with_opt_u32(self, tag: u16, value: Option<u32>) -> Self {
530        match value {
531            Some(v) => self.with(Parameter::u32(tag, v)),
532            None => self,
533        }
534    }
535
536    pub fn with_opt(self, tag: u16, value: Option<Vec<u8>>) -> Self {
537        match value {
538            Some(v) => self.with(Parameter::new(tag, v)),
539            None => self,
540        }
541    }
542
543    /// Encode to the wire. The Message Length includes the common header and every parameter's
544    /// padding.
545    ///
546    /// Refuses anything [`parse_header`] would reject, which is the asymmetry this whole class
547    /// of finding is named after: `MAX_MESSAGE_LEN` bounded the **decode** side, against a
548    /// hostile peer, and nothing bounded the **encode** side, against the model. A message
549    /// whose parameters ran past the ceiling encoded happily and was then rejected by this
550    /// codec's own parser at the far end.
551    pub fn encode(&self) -> Result<Vec<u8>, WireError> {
552        let body_len: usize = self.parameters.iter().map(|p| p.wire_len()).sum();
553        let total = HEADER_LEN + body_len;
554        if total > MAX_MESSAGE_LEN {
555            return Err(WireError::new(
556                ERR_PROTOCOL_ERROR,
557                format!(
558                    "message of {} parameter(s) encodes to {total} octets, past the \
559                     {MAX_MESSAGE_LEN}-octet ceiling this codec's own parser enforces",
560                    self.parameters.len()
561                ),
562            ));
563        }
564        let mut out = Vec::with_capacity(total);
565        out.push(VERSION);
566        out.push(0); // Reserved
567        out.push(self.class);
568        out.push(self.msg_type);
569        // Bounded above, so the cast cannot lose a bit.
570        out.extend_from_slice(&(total as u32).to_be_bytes());
571        for parameter in &self.parameters {
572            parameter.write_into(&mut out)?;
573        }
574        Ok(out)
575    }
576
577    /// Decode a complete message: the common header followed by exactly `length - 8` body
578    /// octets. `full` must be the declared length, which is what `mod.rs`'s reader supplies.
579    pub fn parse(full: &[u8]) -> Result<Message, WireError> {
580        let header = parse_header(full)?;
581        let declared = header.length as usize;
582        if full.len() < declared {
583            return Err(WireError::new(
584                ERR_PROTOCOL_ERROR,
585                format!(
586                    "message declares {declared} octets but only {} were supplied",
587                    full.len()
588                ),
589            ));
590        }
591        let parameters = parse_parameters(&full[HEADER_LEN..declared])?;
592        Ok(Message {
593            class: header.class,
594            msg_type: header.msg_type,
595            parameters,
596        })
597    }
598
599    pub fn param(&self, tag: u16) -> Option<&Parameter> {
600        self.parameters.iter().find(|p| p.tag == tag)
601    }
602
603    pub fn param_u32(&self, tag: u16) -> Option<u32> {
604        self.param(tag).and_then(|p| p.as_u32())
605    }
606
607    pub fn name(&self) -> &'static str {
608        message_name(self.class, self.msg_type)
609    }
610}
611
612/// The (class, type) of an already-encoded message, without decoding it.
613///
614/// The session uses this to learn what a handler's actions actually put on the wire — an
615/// ASPUP ACK is what moves the ASP out of Down, and reading it back off the encoded bytes
616/// means the state machine follows the octets rather than a parallel bookkeeping the actions
617/// could drift from.
618pub fn peek_class_type(bytes: &[u8]) -> Option<(u8, u8)> {
619    if bytes.len() < HEADER_LEN || bytes[0] != VERSION {
620        return None;
621    }
622    Some((bytes[2], bytes[3]))
623}
624
625// ---------------------------------------------------------------------------
626// Protocol Data (RFC 4666 section 3.3.1)
627// ---------------------------------------------------------------------------
628
629/// Fixed part of the Protocol Data value: OPC, DPC, SI, NI, MP, SLS.
630pub const PROTOCOL_DATA_FIXED_LEN: usize = 12;
631
632/// The SS7 routing label plus the user part payload, as structured fields.
633///
634/// Surfacing these separately rather than as one opaque blob is the whole point: a model can
635/// reason about "ISUP from point code 1001 to 2002" and cannot reason about 40 octets of hex.
636#[derive(Clone, Debug, PartialEq, Eq)]
637pub struct ProtocolData {
638    pub opc: u32,
639    pub dpc: u32,
640    pub si: u8,
641    pub ni: u8,
642    pub mp: u8,
643    pub sls: u8,
644    pub payload: Vec<u8>,
645}
646
647impl ProtocolData {
648    pub fn parse(value: &[u8]) -> Result<Self, WireError> {
649        if value.len() < PROTOCOL_DATA_FIXED_LEN {
650            return Err(WireError::new(
651                ERR_PARAMETER_FIELD_ERROR,
652                format!(
653                    "Protocol Data is {} octets, below the {PROTOCOL_DATA_FIXED_LEN}-octet \
654                     routing label",
655                    value.len()
656                ),
657            ));
658        }
659        Ok(Self {
660            opc: u32::from_be_bytes([value[0], value[1], value[2], value[3]]),
661            dpc: u32::from_be_bytes([value[4], value[5], value[6], value[7]]),
662            si: value[8],
663            ni: value[9],
664            mp: value[10],
665            sls: value[11],
666            payload: value[PROTOCOL_DATA_FIXED_LEN..].to_vec(),
667        })
668    }
669
670    /// The Protocol Data parameter *value* — tag, length and padding are added by `Parameter`.
671    pub fn to_value(&self) -> Vec<u8> {
672        let mut out = Vec::with_capacity(PROTOCOL_DATA_FIXED_LEN + self.payload.len());
673        out.extend_from_slice(&self.opc.to_be_bytes());
674        out.extend_from_slice(&self.dpc.to_be_bytes());
675        out.push(self.si);
676        out.push(self.ni);
677        out.push(self.mp);
678        out.push(self.sls);
679        out.extend_from_slice(&self.payload);
680        out
681    }
682}
683
684// ---------------------------------------------------------------------------
685// Builders for the messages an SGP sends
686// ---------------------------------------------------------------------------
687
688/// ASPUP ACK (RFC 4666 section 3.5.4).
689pub fn aspup_ack(info_string: Option<&str>) -> Result<Vec<u8>, WireError> {
690    Message::new(CLASS_ASPSM, ASPSM_ASPUP_ACK)
691        .with_opt(TAG_INFO_STRING, info_string.map(|s| s.as_bytes().to_vec()))
692        .encode()
693}
694
695/// ASPDN ACK (RFC 4666 section 3.5.5).
696pub fn aspdn_ack() -> Result<Vec<u8>, WireError> {
697    Message::new(CLASS_ASPSM, ASPSM_ASPDN_ACK).encode()
698}
699
700/// BEAT ACK (RFC 4666 section 3.5.6). The Heartbeat Data is echoed verbatim, which is the
701/// entire point of the parameter: the ASP matches its own opaque token.
702pub fn beat_ack(heartbeat_data: Option<&[u8]>) -> Result<Vec<u8>, WireError> {
703    Message::new(CLASS_ASPSM, ASPSM_BEAT_ACK)
704        .with_opt(TAG_HEARTBEAT_DATA, heartbeat_data.map(|d| d.to_vec()))
705        .encode()
706}
707
708/// ASPAC ACK (RFC 4666 section 3.7.3).
709pub fn aspac_ack(
710    traffic_mode: Option<u32>,
711    routing_context: Option<u32>,
712    info_string: Option<&str>,
713) -> Result<Vec<u8>, WireError> {
714    Message::new(CLASS_ASPTM, ASPTM_ASPAC_ACK)
715        .with_opt_u32(TAG_TRAFFIC_MODE_TYPE, traffic_mode)
716        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
717        .with_opt(TAG_INFO_STRING, info_string.map(|s| s.as_bytes().to_vec()))
718        .encode()
719}
720
721/// ASPIA ACK (RFC 4666 section 3.7.4).
722pub fn aspia_ack(routing_context: Option<u32>) -> Result<Vec<u8>, WireError> {
723    Message::new(CLASS_ASPTM, ASPTM_ASPIA_ACK)
724        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
725        .encode()
726}
727
728/// ERR (RFC 4666 section 3.8.1).
729///
730/// The Diagnostic Information parameter is deliberately never populated. It is the one field
731/// in M3UA where an internal error string could reach a peer, and the rule in
732/// `crate::utils::wire_failure` is that the peer gets a category and the log gets the error.
733pub fn error(error_code: u32, routing_context: Option<u32>) -> Result<Vec<u8>, WireError> {
734    Message::new(CLASS_MGMT, MGMT_ERR)
735        .with(Parameter::u32(TAG_ERROR_CODE, error_code))
736        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
737        .encode()
738}
739
740/// NTFY (RFC 4666 section 3.8.2). Status is one 32-bit field: type in the high half, info in
741/// the low half.
742pub fn notify(
743    status_type: u16,
744    status_info: u16,
745    asp_identifier: Option<u32>,
746    routing_context: Option<u32>,
747    info_string: Option<&str>,
748) -> Result<Vec<u8>, WireError> {
749    let status = ((status_type as u32) << 16) | status_info as u32;
750    Message::new(CLASS_MGMT, MGMT_NTFY)
751        .with(Parameter::u32(TAG_STATUS, status))
752        .with_opt_u32(TAG_ASP_IDENTIFIER, asp_identifier)
753        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
754        .with_opt(TAG_INFO_STRING, info_string.map(|s| s.as_bytes().to_vec()))
755        .encode()
756}
757
758/// DATA (RFC 4666 section 3.3.1), parameters in the order the RFC lists them.
759pub fn data(
760    protocol_data: &ProtocolData,
761    network_appearance: Option<u32>,
762    routing_context: Option<u32>,
763    correlation_id: Option<u32>,
764) -> Result<Vec<u8>, WireError> {
765    Message::new(CLASS_TRANSFER, TRANSFER_DATA)
766        .with_opt_u32(TAG_NETWORK_APPEARANCE, network_appearance)
767        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
768        .with(Parameter::new(TAG_PROTOCOL_DATA, protocol_data.to_value()))
769        .with_opt_u32(TAG_CORRELATION_ID, correlation_id)
770        .encode()
771}
772
773// ---------------------------------------------------------------------------
774// Builders for the messages an ASP sends (the NetGet M3UA client)
775// ---------------------------------------------------------------------------
776
777/// ASPUP (RFC 4666 section 3.5.1).
778pub fn aspup(asp_identifier: Option<u32>, info_string: Option<&str>) -> Result<Vec<u8>, WireError> {
779    Message::new(CLASS_ASPSM, ASPSM_ASPUP)
780        .with_opt_u32(TAG_ASP_IDENTIFIER, asp_identifier)
781        .with_opt(TAG_INFO_STRING, info_string.map(|s| s.as_bytes().to_vec()))
782        .encode()
783}
784
785/// ASPDN (RFC 4666 section 3.5.2).
786pub fn aspdn() -> Result<Vec<u8>, WireError> {
787    Message::new(CLASS_ASPSM, ASPSM_ASPDN).encode()
788}
789
790/// ASPAC (RFC 4666 section 3.7.1).
791pub fn aspac(
792    traffic_mode: Option<u32>,
793    routing_context: Option<u32>,
794) -> Result<Vec<u8>, WireError> {
795    Message::new(CLASS_ASPTM, ASPTM_ASPAC)
796        .with_opt_u32(TAG_TRAFFIC_MODE_TYPE, traffic_mode)
797        .with_opt_u32(TAG_ROUTING_CONTEXT, routing_context)
798        .encode()
799}
800
801/// BEAT (RFC 4666 section 3.5.5).
802pub fn beat(heartbeat_data: &[u8]) -> Result<Vec<u8>, WireError> {
803    Message::new(CLASS_ASPSM, ASPSM_BEAT)
804        .with(Parameter::new(TAG_HEARTBEAT_DATA, heartbeat_data.to_vec()))
805        .encode()
806}