Skip to main content

netget/server/m3ua/
sccp.rs

1//! SCCP connectionless messages (ITU-T Q.713): UDT, XUDT and their service messages UDTS and
2//! XUDTS, Called/Calling Party Addresses with point code, SSN and global title, and the SCMG
3//! subsystem messages (section 5.3) an SS7 peer exchanges about subsystem status.
4//!
5//! This is the user part carried in an M3UA DATA whose Service Indicator is 3. Everything is in
6//! the ITU format: 14-bit point codes in two octets little-endian, the address indicator's bit 7
7//! the routing indicator, bits 6..3 the global title indicator.
8//!
9//! # What is refused
10//!
11//! - **Segmentation.** An XUDT carrying a Segmentation parameter that is not "first and only
12//!   segment" would need a reassembly buffer keyed by the far end's local reference. NetGet
13//!   refuses it (cause `segmentation not supported`, returned only if the sender asked for
14//!   errors back) instead of buffering, so a peer cannot make it hold partial messages.
15//! - **Long UDT (LUDT)** and the connection-oriented classes 2 and 3: answered by nothing, logged.
16//! - **Data over 255 octets** cannot be expressed in UDT or XUDT (their length field is one
17//!   octet); an outgoing TCAP message that does not fit is refused at encode time rather than
18//!   segmented.
19
20use super::gsm::tbcd_encode;
21use serde_json::{json, Value};
22
23pub const MSG_UDT: u8 = 0x09;
24pub const MSG_UDTS: u8 = 0x0a;
25pub const MSG_XUDT: u8 = 0x11;
26pub const MSG_XUDTS: u8 = 0x12;
27pub const MSG_LUDT: u8 = 0x13;
28pub const MSG_LUDTS: u8 = 0x14;
29
30/// Optional parameter codes (Q.713 section 3).
31pub const PARAM_END_OF_OPTIONAL: u8 = 0x00;
32pub const PARAM_SEGMENTATION: u8 = 0x10;
33pub const PARAM_IMPORTANCE: u8 = 0x12;
34
35/// Return causes (Q.713 section 3.12).
36pub const CAUSE_NO_TRANSLATION_FOR_NATURE: u8 = 0x00;
37pub const CAUSE_NO_TRANSLATION_FOR_ADDRESS: u8 = 0x01;
38pub const CAUSE_SUBSYSTEM_FAILURE: u8 = 0x03;
39pub const CAUSE_UNEQUIPPED_USER: u8 = 0x04;
40pub const CAUSE_UNQUALIFIED: u8 = 0x07;
41pub const CAUSE_ERROR_IN_LOCAL_PROCESSING: u8 = 0x09;
42pub const CAUSE_SEGMENTATION_NOT_SUPPORTED: u8 = 0x0d;
43
44/// SSN 1 is SCCP management itself.
45pub const SSN_SCMG: u8 = 1;
46/// SCMG format identifiers (Q.713 section 5.3).
47pub const SCMG_SSA: u8 = 0x01;
48pub const SCMG_SSP: u8 = 0x02;
49pub const SCMG_SST: u8 = 0x03;
50
51/// The largest data part a UDT or XUDT can carry: its length is a single octet.
52pub const MAX_DATA_LEN: usize = 255;
53/// Hop counter NetGet puts in the XUDTs it originates (Q.714 recommends 15 or below).
54pub const DEFAULT_HOP_COUNTER: u8 = 15;
55
56fn err<T>(msg: impl Into<String>) -> Result<T, String> {
57    Err(msg.into())
58}
59
60/// A global title in structured form.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct GlobalTitle {
63    /// Global title indicator 1..4.
64    pub gti: u8,
65    pub translation_type: u8,
66    /// Numbering plan (GTI 3 and 4); 1 is ISDN/telephony.
67    pub numbering_plan: u8,
68    /// Nature of address indicator (GTI 1 and 4); 4 is international.
69    pub nature: u8,
70    pub digits: String,
71}
72
73impl GlobalTitle {
74    pub fn e164(digits: &str) -> Self {
75        GlobalTitle {
76            gti: 4,
77            translation_type: 0,
78            numbering_plan: 1,
79            nature: 4,
80            digits: digits.to_string(),
81        }
82    }
83}
84
85/// A Called or Calling Party Address.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct SccpAddress {
88    /// Routing indicator: true routes on the global title, false on DPC + SSN.
89    pub route_on_gt: bool,
90    pub point_code: Option<u16>,
91    pub ssn: Option<u8>,
92    pub global_title: Option<GlobalTitle>,
93    /// Bit 8 of the address indicator, reserved for national use; kept so a reply echoes it.
94    pub national: bool,
95}
96
97impl SccpAddress {
98    pub fn decode(o: &[u8]) -> Result<Self, String> {
99        let Some((&ai, mut rest)) = o.split_first() else {
100            return err("empty SCCP address");
101        };
102        let pc_present = ai & 0x01 != 0;
103        let ssn_present = ai & 0x02 != 0;
104        let gti = (ai >> 2) & 0x0f;
105        let route_on_gt = ai & 0x40 == 0;
106        let point_code = if pc_present {
107            if rest.len() < 2 {
108                return err("SCCP address indicates a point code but has no room for it");
109            }
110            let pc = u16::from_le_bytes([rest[0], rest[1]]) & 0x3fff;
111            rest = &rest[2..];
112            Some(pc)
113        } else {
114            None
115        };
116        let ssn = if ssn_present {
117            let Some((&s, r)) = rest.split_first() else {
118                return err("SCCP address indicates an SSN but has no room for it");
119            };
120            rest = r;
121            Some(s)
122        } else {
123            None
124        };
125        let global_title = match gti {
126            0 => None,
127            1 => {
128                let Some((&b, digits)) = rest.split_first() else {
129                    return err("GTI 1 global title is empty");
130                };
131                let mut d = bcd_digits(digits);
132                if b & 0x80 != 0 {
133                    d.pop();
134                }
135                Some(GlobalTitle {
136                    gti,
137                    translation_type: 0,
138                    numbering_plan: 0,
139                    nature: b & 0x7f,
140                    digits: d,
141                })
142            }
143            2 => {
144                let Some((&tt, digits)) = rest.split_first() else {
145                    return err("GTI 2 global title is empty");
146                };
147                Some(GlobalTitle {
148                    gti,
149                    translation_type: tt,
150                    numbering_plan: 0,
151                    nature: 0,
152                    digits: bcd_digits(digits),
153                })
154            }
155            3 | 4 => {
156                let need = if gti == 4 { 3 } else { 2 };
157                if rest.len() < need {
158                    return err(format!("GTI {gti} global title shorter than its header"));
159                }
160                let tt = rest[0];
161                let np = rest[1] >> 4;
162                let es = rest[1] & 0x0f;
163                let nature = if gti == 4 { rest[2] & 0x7f } else { 0 };
164                let digits = &rest[need..];
165                let mut d = bcd_digits(digits);
166                // Encoding scheme 1 is BCD with an odd number of digits: the last high nibble is
167                // filler, whatever value the sender put there.
168                if es == 1 && !d.is_empty() {
169                    d.pop();
170                } else if es != 1 && es != 2 {
171                    return err(format!(
172                        "GTI {gti} global title uses encoding scheme {es}; only BCD (1, 2) is \
173                         implemented"
174                    ));
175                }
176                Some(GlobalTitle {
177                    gti,
178                    translation_type: tt,
179                    numbering_plan: np,
180                    nature,
181                    digits: d,
182                })
183            }
184            other => return err(format!("global title indicator {other} is reserved")),
185        };
186        if route_on_gt && global_title.is_none() {
187            return err("SCCP address routes on global title but carries none");
188        }
189        Ok(SccpAddress {
190            route_on_gt,
191            point_code,
192            ssn,
193            global_title,
194            national: ai & 0x80 != 0,
195        })
196    }
197
198    pub fn encode(&self) -> Result<Vec<u8>, String> {
199        let gti = self.global_title.as_ref().map(|g| g.gti).unwrap_or(0);
200        let mut ai = (gti & 0x0f) << 2;
201        if self.point_code.is_some() {
202            ai |= 0x01;
203        }
204        if self.ssn.is_some() {
205            ai |= 0x02;
206        }
207        if !self.route_on_gt {
208            ai |= 0x40;
209        }
210        if self.national {
211            ai |= 0x80;
212        }
213        if self.route_on_gt && self.global_title.is_none() {
214            return err("an address that routes on global title needs one");
215        }
216        if !self.route_on_gt && self.ssn.is_none() {
217            return err("an address that routes on SSN needs an ssn");
218        }
219        let mut out = vec![ai];
220        if let Some(pc) = self.point_code {
221            if pc > 0x3fff {
222                return err(format!("point code {pc} does not fit 14 bits"));
223            }
224            out.extend(pc.to_le_bytes());
225        }
226        if let Some(ssn) = self.ssn {
227            out.push(ssn);
228        }
229        if let Some(g) = &self.global_title {
230            if g.digits.is_empty() || g.digits.len() > 32 {
231                return err("a global title needs 1..32 digits");
232            }
233            let odd = g.digits.len() % 2 == 1;
234            let digits = tbcd_encode(&g.digits)?;
235            // Odd counts are filled with 0 in SCCP, not with TBCD's F.
236            let digits: Vec<u8> = if odd {
237                let mut d = digits;
238                if let Some(last) = d.last_mut() {
239                    *last &= 0x0f;
240                }
241                d
242            } else {
243                digits
244            };
245            match g.gti {
246                1 => {
247                    out.push((odd as u8) << 7 | (g.nature & 0x7f));
248                }
249                2 => out.push(g.translation_type),
250                3 => {
251                    out.push(g.translation_type);
252                    out.push(g.numbering_plan << 4 | if odd { 1 } else { 2 });
253                }
254                4 => {
255                    out.push(g.translation_type);
256                    out.push(g.numbering_plan << 4 | if odd { 1 } else { 2 });
257                    out.push(g.nature & 0x7f);
258                }
259                other => return err(format!("global title indicator {other} is not 1..4")),
260            }
261            out.extend(digits);
262        }
263        Ok(out)
264    }
265
266    pub fn to_json(&self) -> Value {
267        let mut v = json!({
268            "route_on": if self.route_on_gt { "gt" } else { "ssn" },
269            "pc": self.point_code,
270            "ssn": self.ssn,
271        });
272        if let Some(g) = &self.global_title {
273            v["gt"] = json!({
274                "digits": g.digits,
275                "gti": g.gti,
276                "tt": g.translation_type,
277                "np": g.numbering_plan,
278                "nai": g.nature,
279            });
280        }
281        v
282    }
283
284    /// From `{"route_on": "gt"|"ssn", "pc", "ssn", "gt": "digits" | {"digits","tt","np","nai","gti"}}`.
285    pub fn from_json(v: &Value, field: &str) -> Result<Self, String> {
286        let o = v
287            .as_object()
288            .ok_or_else(|| format!("{field} must be an object {{route_on, pc, ssn, gt}}"))?;
289        let num = |k: &str, max: u64| -> Result<Option<u64>, String> {
290            match o.get(k) {
291                None | Some(Value::Null) => Ok(None),
292                Some(x) => x
293                    .as_u64()
294                    .filter(|n| *n <= max)
295                    .map(Some)
296                    .ok_or_else(|| format!("{field}.{k} must be an integer 0..{max}")),
297            }
298        };
299        let global_title = match o.get("gt") {
300            None | Some(Value::Null) => None,
301            Some(Value::String(s)) => Some(GlobalTitle::e164(s.trim_start_matches('+'))),
302            Some(Value::Object(g)) => {
303                let get = |k: &str, d: u8, max: u8| -> Result<u8, String> {
304                    match g.get(k) {
305                        None => Ok(d),
306                        Some(x) => x
307                            .as_u64()
308                            .filter(|n| *n <= max as u64)
309                            .map(|n| n as u8)
310                            .ok_or_else(|| format!("{field}.gt.{k} must be 0..{max}")),
311                    }
312                };
313                Some(GlobalTitle {
314                    gti: get("gti", 4, 4)?.max(1),
315                    translation_type: get("tt", 0, 255)?,
316                    numbering_plan: get("np", 1, 15)?,
317                    nature: get("nai", 4, 127)?,
318                    digits: g
319                        .get("digits")
320                        .and_then(Value::as_str)
321                        .ok_or_else(|| format!("{field}.gt.digits is required"))?
322                        .trim_start_matches('+')
323                        .to_string(),
324                })
325            }
326            Some(_) => return Err(format!("{field}.gt must be digits or an object")),
327        };
328        let route_on_gt = match o.get("route_on").and_then(Value::as_str) {
329            Some("gt") => true,
330            Some("ssn") => false,
331            Some(other) => return Err(format!("{field}.route_on {other:?} is not gt or ssn")),
332            None => global_title.is_some() && o.get("pc").is_none(),
333        };
334        let a = SccpAddress {
335            route_on_gt,
336            point_code: num("pc", 0x3fff)?.map(|n| n as u16),
337            ssn: num("ssn", 255)?.map(|n| n as u8),
338            global_title,
339            national: false,
340        };
341        a.encode()?;
342        Ok(a)
343    }
344}
345
346/// BCD digits as SCCP writes them (filler 0 for odd counts is handled by the caller).
347fn bcd_digits(o: &[u8]) -> String {
348    let mut out = String::new();
349    for b in o {
350        for n in [b & 0x0f, b >> 4] {
351            out.push(match n {
352                0..=9 => (b'0' + n) as char,
353                0xb => '*',
354                0xc => '#',
355                0xa => 'a',
356                0xd => 'b',
357                0xe => 'c',
358                _ => 'f',
359            });
360        }
361    }
362    out
363}
364
365/// A Segmentation parameter (Q.713 section 3.17).
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub struct Segmentation {
368    pub first: bool,
369    pub remaining: u8,
370    pub local_reference: u32,
371}
372
373impl Segmentation {
374    /// "First segment, no more to come" carries the whole message and needs no reassembly.
375    pub fn is_whole_message(&self) -> bool {
376        self.first && self.remaining == 0
377    }
378}
379
380/// A decoded connectionless SCCP message.
381#[derive(Clone, Debug, PartialEq, Eq)]
382pub struct Unitdata {
383    pub message_type: u8,
384    /// Protocol class 0 or 1 (low nibble of the protocol class octet).
385    pub protocol_class: u8,
386    /// Return-on-error: the sender wants a UDTS/XUDTS if delivery fails.
387    pub return_on_error: bool,
388    /// UDTS/XUDTS only.
389    pub return_cause: Option<u8>,
390    pub hop_counter: Option<u8>,
391    pub called: SccpAddress,
392    pub calling: SccpAddress,
393    pub data: Vec<u8>,
394    pub segmentation: Option<Segmentation>,
395    pub importance: Option<u8>,
396}
397
398impl Unitdata {
399    pub fn is_service_message(&self) -> bool {
400        matches!(self.message_type, MSG_UDTS | MSG_XUDTS)
401    }
402
403    pub fn type_name(&self) -> &'static str {
404        message_type_name(self.message_type)
405    }
406}
407
408pub fn message_type_name(t: u8) -> &'static str {
409    match t {
410        MSG_UDT => "UDT",
411        MSG_UDTS => "UDTS",
412        MSG_XUDT => "XUDT",
413        MSG_XUDTS => "XUDTS",
414        MSG_LUDT => "LUDT",
415        MSG_LUDTS => "LUDTS",
416        0x01 => "CR",
417        0x02 => "CC",
418        0x03 => "CREF",
419        0x04 => "RLSD",
420        0x05 => "RLC",
421        0x06 => "DT1",
422        _ => "unknown",
423    }
424}
425
426/// Follow a one-octet pointer at `at` to a length-prefixed field.
427fn variable(o: &[u8], at: usize, name: &str) -> Result<Vec<u8>, String> {
428    let ptr = *o
429        .get(at)
430        .ok_or_else(|| format!("SCCP message ends before the {name} pointer"))?
431        as usize;
432    if ptr == 0 {
433        return err(format!("SCCP {name} pointer is zero"));
434    }
435    let start = at + ptr;
436    let len = *o
437        .get(start)
438        .ok_or_else(|| format!("SCCP {name} pointer leads past the end"))? as usize;
439    o.get(start + 1..start + 1 + len)
440        .map(|s| s.to_vec())
441        .ok_or_else(|| format!("SCCP {name} of {len} octets runs past the end"))
442}
443
444/// Decode a UDT, XUDT, UDTS or XUDTS. Other types are reported by name in the error.
445pub fn decode(o: &[u8]) -> Result<Unitdata, String> {
446    let Some(&mt) = o.first() else {
447        return err("empty SCCP message");
448    };
449    let (class_octet, cause, hop, ptr_base, has_optional) = match mt {
450        MSG_UDT => (
451            Some(*o.get(1).ok_or("UDT without protocol class")?),
452            None,
453            None,
454            2,
455            false,
456        ),
457        MSG_UDTS => (
458            None,
459            Some(*o.get(1).ok_or("UDTS without return cause")?),
460            None,
461            2,
462            false,
463        ),
464        MSG_XUDT => (
465            Some(*o.get(1).ok_or("XUDT without protocol class")?),
466            None,
467            Some(*o.get(2).ok_or("XUDT without hop counter")?),
468            3,
469            true,
470        ),
471        MSG_XUDTS => (
472            None,
473            Some(*o.get(1).ok_or("XUDTS without return cause")?),
474            Some(*o.get(2).ok_or("XUDTS without hop counter")?),
475            3,
476            true,
477        ),
478        other => {
479            return err(format!(
480                "SCCP {} (message type 0x{other:02x}) is not implemented; NetGet handles UDT, \
481                 XUDT, UDTS and XUDTS",
482                message_type_name(other)
483            ))
484        }
485    };
486    let called = SccpAddress::decode(&variable(o, ptr_base, "called party address")?)?;
487    let calling = SccpAddress::decode(&variable(o, ptr_base + 1, "calling party address")?)?;
488    let data = variable(o, ptr_base + 2, "data")?;
489    let mut segmentation = None;
490    let mut importance = None;
491    if has_optional {
492        let ptr = *o
493            .get(ptr_base + 3)
494            .ok_or("XUDT ends before its optional pointer")? as usize;
495        if ptr != 0 {
496            let mut pos = ptr_base + 3 + ptr;
497            // At most a handful of optional parameters exist; bound the walk anyway.
498            for _ in 0..16 {
499                let Some(&code) = o.get(pos) else {
500                    return err("XUDT optional part ends without end-of-optional-parameters");
501                };
502                if code == PARAM_END_OF_OPTIONAL {
503                    break;
504                }
505                let len = *o
506                    .get(pos + 1)
507                    .ok_or("XUDT optional parameter without length")?
508                    as usize;
509                let v = o
510                    .get(pos + 2..pos + 2 + len)
511                    .ok_or("XUDT optional parameter runs past the end")?;
512                match code {
513                    PARAM_SEGMENTATION if len == 4 => {
514                        segmentation = Some(Segmentation {
515                            first: v[0] & 0x80 != 0,
516                            remaining: v[0] & 0x0f,
517                            local_reference: u32::from_be_bytes([0, v[1], v[2], v[3]]),
518                        });
519                    }
520                    PARAM_IMPORTANCE if len == 1 => importance = Some(v[0] & 0x07),
521                    _ => {}
522                }
523                pos += 2 + len;
524            }
525        }
526    }
527    let class = class_octet.unwrap_or(0);
528    Ok(Unitdata {
529        message_type: mt,
530        protocol_class: class & 0x0f,
531        return_on_error: class & 0x80 != 0,
532        return_cause: cause,
533        hop_counter: hop,
534        called,
535        calling,
536        data,
537        segmentation,
538        importance,
539    })
540}
541
542/// Encode a UDT (protocol class 0 or 1).
543pub fn encode_udt(
544    protocol_class: u8,
545    return_on_error: bool,
546    called: &SccpAddress,
547    calling: &SccpAddress,
548    data: &[u8],
549) -> Result<Vec<u8>, String> {
550    let class = (protocol_class & 0x01) | if return_on_error { 0x80 } else { 0 };
551    encode_unitdata(MSG_UDT, &[class], called, calling, data, false)
552}
553
554/// Encode an XUDT with the given hop counter and no optional parameters.
555pub fn encode_xudt(
556    protocol_class: u8,
557    return_on_error: bool,
558    hop_counter: u8,
559    called: &SccpAddress,
560    calling: &SccpAddress,
561    data: &[u8],
562) -> Result<Vec<u8>, String> {
563    let class = (protocol_class & 0x01) | if return_on_error { 0x80 } else { 0 };
564    encode_unitdata(
565        MSG_XUDT,
566        &[class, hop_counter.clamp(1, 15)],
567        called,
568        calling,
569        data,
570        true,
571    )
572}
573
574/// The service message answering `original` with `cause`: UDTS for a UDT, XUDTS for an XUDT.
575/// Addresses are swapped, the data is returned (truncated to what fits).
576pub fn encode_service(original: &Unitdata, cause: u8) -> Result<Vec<u8>, String> {
577    let mut data = original.data.clone();
578    data.truncate(MAX_DATA_LEN);
579    match original.message_type {
580        MSG_XUDT => encode_unitdata(
581            MSG_XUDTS,
582            &[cause, DEFAULT_HOP_COUNTER],
583            &original.calling,
584            &original.called,
585            &data,
586            true,
587        ),
588        _ => encode_unitdata(
589            MSG_UDTS,
590            &[cause],
591            &original.calling,
592            &original.called,
593            &data,
594            false,
595        ),
596    }
597}
598
599fn encode_unitdata(
600    mt: u8,
601    fixed: &[u8],
602    called: &SccpAddress,
603    calling: &SccpAddress,
604    data: &[u8],
605    optional_pointer: bool,
606) -> Result<Vec<u8>, String> {
607    if data.len() > MAX_DATA_LEN {
608        return err(format!(
609            "{} octets of data do not fit one {} ({MAX_DATA_LEN} at most); NetGet does not \
610             segment",
611            data.len(),
612            message_type_name(mt)
613        ));
614    }
615    let called = called.encode()?;
616    let calling = calling.encode()?;
617    let pointers = if optional_pointer { 4 } else { 3 };
618    let mut out = vec![mt];
619    out.extend_from_slice(fixed);
620    // Each pointer counts from its own octet to the length octet of its field.
621    let p_called = pointers;
622    let p_calling = p_called - 1 + 1 + called.len();
623    let p_data = p_calling - 1 + 1 + calling.len();
624    for p in [p_called, p_calling, p_data] {
625        if p > 255 {
626            return err("SCCP addresses too long for one-octet pointers");
627        }
628    }
629    out.push(p_called as u8);
630    out.push(p_calling as u8);
631    out.push(p_data as u8);
632    if optional_pointer {
633        out.push(0);
634    }
635    out.push(called.len() as u8);
636    out.extend(called);
637    out.push(calling.len() as u8);
638    out.extend(calling);
639    out.push(data.len() as u8);
640    out.extend_from_slice(data);
641    Ok(out)
642}
643
644/// A decoded SCMG message (Q.713 section 5.3), the data of a UDT to SSN 1.
645#[derive(Clone, Copy, Debug, PartialEq, Eq)]
646pub struct Scmg {
647    pub format: u8,
648    pub affected_ssn: u8,
649    pub affected_pc: u16,
650    pub multiplicity: u8,
651}
652
653impl Scmg {
654    pub fn decode(o: &[u8]) -> Result<Self, String> {
655        if o.len() < 5 {
656            return err(format!("SCMG message of {} octets (5 expected)", o.len()));
657        }
658        Ok(Scmg {
659            format: o[0],
660            affected_ssn: o[1],
661            affected_pc: u16::from_le_bytes([o[2], o[3]]) & 0x3fff,
662            multiplicity: o[4] & 0x03,
663        })
664    }
665
666    pub fn encode(&self) -> Vec<u8> {
667        let pc = self.affected_pc.to_le_bytes();
668        vec![
669            self.format,
670            self.affected_ssn,
671            pc[0],
672            pc[1],
673            self.multiplicity,
674        ]
675    }
676
677    pub fn name(&self) -> &'static str {
678        match self.format {
679            SCMG_SSA => "SSA",
680            SCMG_SSP => "SSP",
681            SCMG_SST => "SST",
682            0x04 => "SOR",
683            0x05 => "SOG",
684            0x06 => "SSC",
685            _ => "unknown",
686        }
687    }
688}