Skip to main content

netget/server/pfcp/
wire.rs

1//! PFCP (3GPP TS 29.244) framing and information elements, shared by the UPF-side server and
2//! the SMF-side client (`src/client/pfcp/`).
3//!
4//! IEs are carried to and from the model as a JSON object keyed by IE name. A grouped IE is an
5//! object of the same shape, and a key whose value is an array is that IE repeated
6//! (`"create_pdr": [{…}, {…}]`). The table below gives the IEs a model needs for associations
7//! and sessions their own readable forms (addresses, flag names, cause names). Any other IE
8//! keeps its number and its bytes, as `"ie_<type>": {"hex": "…"}`, so nothing is lost on the
9//! way through.
10use anyhow::{bail, ensure, Context, Result};
11use serde_json::{json, Map, Value};
12use std::net::{Ipv4Addr, Ipv6Addr};
13
14pub const PORT: u16 = 8805;
15pub const MAX_DATAGRAM: usize = 65_507;
16/// Grouped IEs nest at most this deep (Create PDR > PDI > … is three or four).
17pub const MAX_DEPTH: usize = 8;
18/// IEs in one message, counting every nested one.
19pub const MAX_IES: usize = 1024;
20/// Seconds between 1900 (NTP) and 1970.
21const NTP_UNIX: u64 = 2_208_988_800;
22
23pub const MESSAGES: &[(u8, &str)] = &[
24    (1, "heartbeat_request"),
25    (2, "heartbeat_response"),
26    (3, "pfd_management_request"),
27    (4, "pfd_management_response"),
28    (5, "association_setup_request"),
29    (6, "association_setup_response"),
30    (7, "association_update_request"),
31    (8, "association_update_response"),
32    (9, "association_release_request"),
33    (10, "association_release_response"),
34    (11, "version_not_supported_response"),
35    (12, "node_report_request"),
36    (13, "node_report_response"),
37    (14, "session_set_deletion_request"),
38    (15, "session_set_deletion_response"),
39    (50, "session_establishment_request"),
40    (51, "session_establishment_response"),
41    (52, "session_modification_request"),
42    (53, "session_modification_response"),
43    (54, "session_deletion_request"),
44    (55, "session_deletion_response"),
45    (56, "session_report_request"),
46    (57, "session_report_response"),
47];
48
49pub fn message_name(t: u8) -> String {
50    MESSAGES
51        .iter()
52        .find(|(n, _)| *n == t)
53        .map(|(_, s)| s.to_string())
54        .unwrap_or_else(|| format!("message_{t}"))
55}
56
57pub fn message_type(name: &str) -> Result<u8> {
58    MESSAGES
59        .iter()
60        .find(|(_, s)| *s == name)
61        .map(|(n, _)| *n)
62        .with_context(|| format!("{name:?} is not a PFCP message name"))
63}
64
65/// Session-related messages (type ≥ 50) carry a SEID in their header.
66pub fn has_seid(t: u8) -> bool {
67    t >= 50
68}
69
70/// The request types (TS 29.244 §7.3); each one's response is the next number.
71pub fn is_request(t: u8) -> bool {
72    matches!(t, 1 | 3 | 5 | 7 | 9 | 12 | 14 | 16 | 50 | 52 | 54 | 56)
73}
74
75pub const CAUSES: &[(u8, &str)] = &[
76    (1, "request_accepted"),
77    (2, "more_usage_report_to_send"),
78    (64, "request_rejected"),
79    (65, "session_context_not_found"),
80    (66, "mandatory_ie_missing"),
81    (67, "conditional_ie_missing"),
82    (68, "invalid_length"),
83    (69, "mandatory_ie_incorrect"),
84    (70, "invalid_forwarding_policy"),
85    (71, "invalid_f_teid_allocation_option"),
86    (72, "no_established_pfcp_association"),
87    (73, "rule_creation_modification_failure"),
88    (74, "pfcp_entity_in_congestion"),
89    (75, "no_resources_available"),
90    (76, "service_not_supported"),
91    (77, "system_failure"),
92    (78, "redirection_requested"),
93];
94
95pub fn cause_name(c: u8) -> String {
96    CAUSES
97        .iter()
98        .find(|(n, _)| *n == c)
99        .map(|(_, s)| s.to_string())
100        .unwrap_or_else(|| format!("cause_{c}"))
101}
102
103pub fn cause_code(name: &str) -> Result<u8> {
104    CAUSES
105        .iter()
106        .find(|(_, s)| *s == name)
107        .map(|(n, _)| *n)
108        .or_else(|| name.strip_prefix("cause_").and_then(|n| n.parse().ok()))
109        .with_context(|| {
110            format!(
111                "cause {name:?} is not one of {}",
112                CAUSES
113                    .iter()
114                    .map(|(_, s)| *s)
115                    .collect::<Vec<_>>()
116                    .join(", ")
117            )
118        })
119}
120
121#[derive(Clone, Copy, PartialEq, Eq, Debug)]
122enum K {
123    Grouped,
124    U8,
125    U16,
126    U32,
127    Cause,
128    NodeId,
129    FSeid,
130    FTeid,
131    UeIp,
132    ApplyAction,
133    Interface,
134    Text,
135    Recovery,
136    OuterHeaderCreation,
137    OuterHeaderRemoval,
138    Hex,
139}
140
141const IES: &[(u16, &str, K)] = &[
142    (1, "create_pdr", K::Grouped),
143    (2, "pdi", K::Grouped),
144    (3, "create_far", K::Grouped),
145    (4, "forwarding_parameters", K::Grouped),
146    (5, "duplicating_parameters", K::Grouped),
147    (6, "create_urr", K::Grouped),
148    (7, "create_qer", K::Grouped),
149    (8, "created_pdr", K::Grouped),
150    (9, "update_pdr", K::Grouped),
151    (10, "update_far", K::Grouped),
152    (11, "update_forwarding_parameters", K::Grouped),
153    (12, "update_bar_session_report_response", K::Grouped),
154    (13, "update_urr", K::Grouped),
155    (14, "update_qer", K::Grouped),
156    (15, "remove_pdr", K::Grouped),
157    (16, "remove_far", K::Grouped),
158    (17, "remove_urr", K::Grouped),
159    (18, "remove_qer", K::Grouped),
160    (19, "cause", K::Cause),
161    (20, "source_interface", K::Interface),
162    (21, "f_teid", K::FTeid),
163    (22, "network_instance", K::Text),
164    (23, "sdf_filter", K::Hex),
165    (24, "application_id", K::Text),
166    (25, "gate_status", K::U8),
167    (26, "mbr", K::Hex),
168    (27, "gbr", K::Hex),
169    (28, "qer_correlation_id", K::U32),
170    (29, "precedence", K::U32),
171    (37, "measurement_method", K::U8),
172    (39, "report_type", K::U8),
173    (40, "offending_ie", K::U16),
174    (42, "destination_interface", K::Interface),
175    (43, "up_function_features", K::Hex),
176    (44, "apply_action", K::ApplyAction),
177    (54, "downlink_data_service_information", K::Hex),
178    (56, "pdr_id", K::U16),
179    (57, "f_seid", K::FSeid),
180    (60, "node_id", K::NodeId),
181    (81, "urr_id", K::U32),
182    (83, "downlink_data_report", K::Grouped),
183    (84, "outer_header_creation", K::OuterHeaderCreation),
184    (85, "create_bar", K::Grouped),
185    (86, "update_bar", K::Grouped),
186    (87, "remove_bar", K::Grouped),
187    (88, "bar_id", K::U8),
188    (89, "cp_function_features", K::Hex),
189    (93, "ue_ip_address", K::UeIp),
190    (95, "outer_header_removal", K::OuterHeaderRemoval),
191    (96, "recovery_time_stamp", K::Recovery),
192    (108, "far_id", K::U32),
193    (109, "qer_id", K::U32),
194    (111, "pdn_type", K::U8),
195    (114, "failed_rule_id", K::Hex),
196    (124, "qfi", K::U8),
197    (155, "network_instance_dnn", K::Text),
198];
199
200fn by_type(t: u16) -> Option<(&'static str, K)> {
201    IES.iter()
202        .find(|(n, _, _)| *n == t)
203        .map(|(_, s, k)| (*s, *k))
204}
205
206fn by_name(name: &str) -> Result<(u16, K)> {
207    if let Some((t, _, k)) = IES.iter().find(|(_, s, _)| *s == name) {
208        return Ok((*t, *k));
209    }
210    if let Some(n) = name.strip_prefix("ie_").and_then(|n| n.parse().ok()) {
211        return Ok((n, K::Hex));
212    }
213    bail!("{name:?} is not a PFCP IE NetGet knows; give unknown ones as ie_<type> with {{\"hex\": ...}}")
214}
215
216const INTERFACES: &[&str] = &[
217    "access",
218    "core",
219    "sgi_lan_n6_lan",
220    "cp_function",
221    "5g_vn_internal",
222];
223const APPLY: &[&str] = &[
224    "DROP", "FORW", "BUFF", "NOCP", "DUPL", "IPMA", "IPMD", "DFRT", "EDRT", "BDPN", "DDPN",
225];
226const OHC: &[&str] = &[
227    "gtpu_udp_ipv4",
228    "gtpu_udp_ipv6",
229    "udp_ipv4",
230    "udp_ipv6",
231    "ipv4",
232    "ipv6",
233    "c_tag",
234    "s_tag",
235];
236
237#[derive(Debug, Clone, PartialEq)]
238pub struct Header {
239    pub message_type: u8,
240    pub seid: Option<u64>,
241    pub sequence: u32,
242}
243
244#[derive(Debug, Clone)]
245pub struct Message {
246    pub header: Header,
247    /// The IEs in the model's form.
248    pub ies: Value,
249}
250
251/// Parse one datagram. Further messages after the first (follow-on) are ignored.
252pub fn parse(b: &[u8]) -> Result<Message> {
253    ensure!(b.len() <= MAX_DATAGRAM, "a {}-byte datagram", b.len());
254    ensure!(
255        b.len() >= 8,
256        "a {}-byte datagram is shorter than a PFCP header",
257        b.len()
258    );
259    let version = b[0] >> 5;
260    ensure!(version == 1, "PFCP version {version}");
261    let s = b[0] & 0x01 != 0;
262    let message_type = b[1];
263    let length = u16::from_be_bytes([b[2], b[3]]) as usize;
264    ensure!(
265        4 + length <= b.len(),
266        "Message Length {length} runs past the datagram"
267    );
268    let body = &b[4..4 + length];
269    let (seid, rest) = if s {
270        ensure!(body.len() >= 12, "a truncated header");
271        (Some(u64::from_be_bytes(body[..8].try_into()?)), &body[8..])
272    } else {
273        ensure!(body.len() >= 4, "a truncated header");
274        (None, body)
275    };
276    let sequence = u32::from_be_bytes([0, rest[0], rest[1], rest[2]]);
277    let mut count = 0;
278    let ies = decode_ies(&rest[4..], 0, &mut count)?;
279    Ok(Message {
280        header: Header {
281            message_type,
282            seid,
283            sequence,
284        },
285        ies,
286    })
287}
288
289fn decode_ies(mut b: &[u8], depth: usize, count: &mut usize) -> Result<Value> {
290    ensure!(
291        depth < MAX_DEPTH,
292        "grouped IEs nest more than {MAX_DEPTH} deep"
293    );
294    let mut out = Map::new();
295    let mut repeated = std::collections::HashSet::new();
296    while !b.is_empty() {
297        ensure!(b.len() >= 4, "a truncated IE header");
298        let t = u16::from_be_bytes([b[0], b[1]]);
299        let len = u16::from_be_bytes([b[2], b[3]]) as usize;
300        ensure!(4 + len <= b.len(), "IE {t} runs past its container");
301        *count += 1;
302        ensure!(*count <= MAX_IES, "more than {MAX_IES} IEs");
303        // Enterprise-specific IEs (bit 15) carry a 2-byte enterprise id first; kept as bytes.
304        let v = &b[4..4 + len];
305        let (name, value) = match by_type(t) {
306            Some((name, kind)) if t & 0x8000 == 0 => (
307                name.to_string(),
308                decode_value(kind, v, depth, count).with_context(|| format!("IE {name}"))?,
309            ),
310            _ => (format!("ie_{t}"), json!({"hex": hex::encode(v)})),
311        };
312        match out.get_mut(&name) {
313            None => {
314                out.insert(name, value);
315            }
316            Some(existing) => {
317                // A repeated IE becomes an array of its values.
318                if repeated.contains(&name) {
319                    if let Value::Array(list) = existing {
320                        list.push(value);
321                    }
322                } else {
323                    let first = existing.take();
324                    *existing = Value::Array(vec![first, value]);
325                    repeated.insert(name);
326                }
327            }
328        }
329        b = &b[4 + len..];
330    }
331    Ok(Value::Object(out))
332}
333
334fn ip4(b: &[u8]) -> Result<Ipv4Addr> {
335    ensure!(b.len() >= 4, "a truncated IPv4 address");
336    Ok(Ipv4Addr::new(b[0], b[1], b[2], b[3]))
337}
338
339fn ip6(b: &[u8]) -> Result<Ipv6Addr> {
340    ensure!(b.len() >= 16, "a truncated IPv6 address");
341    let a: [u8; 16] = b[..16].try_into()?;
342    Ok(Ipv6Addr::from(a))
343}
344
345fn flags(bits: u32, names: &[&str]) -> Vec<String> {
346    names
347        .iter()
348        .enumerate()
349        .filter(|(i, _)| bits & (1 << i) != 0)
350        .map(|(_, n)| n.to_string())
351        .collect()
352}
353
354/// A Network Instance: DNS label encoding when it parses as one, else the text.
355fn text(v: &[u8]) -> String {
356    let mut labels = Vec::new();
357    let mut i = 0;
358    while i < v.len() {
359        let n = v[i] as usize;
360        if n == 0
361            || i + 1 + n > v.len()
362            || !v[i + 1..i + 1 + n].iter().all(|c| c.is_ascii_graphic())
363        {
364            return String::from_utf8_lossy(v).into_owned();
365        }
366        labels.push(String::from_utf8_lossy(&v[i + 1..i + 1 + n]).into_owned());
367        i += 1 + n;
368    }
369    if labels.len() > 1 {
370        labels.join(".")
371    } else {
372        String::from_utf8_lossy(v).into_owned()
373    }
374}
375
376fn decode_value(kind: K, v: &[u8], depth: usize, count: &mut usize) -> Result<Value> {
377    let need = |n: usize| -> Result<()> {
378        ensure!(v.len() >= n, "{} bytes where {n} are needed", v.len());
379        Ok(())
380    };
381    Ok(match kind {
382        K::Grouped => decode_ies(v, depth + 1, count)?,
383        K::U8 => {
384            need(1)?;
385            json!(v[0])
386        }
387        K::U16 => {
388            need(2)?;
389            json!(u16::from_be_bytes([v[0], v[1]]))
390        }
391        K::U32 => {
392            need(4)?;
393            json!(u32::from_be_bytes(v[..4].try_into()?))
394        }
395        K::Cause => {
396            need(1)?;
397            json!(cause_name(v[0]))
398        }
399        K::NodeId => {
400            need(1)?;
401            match v[0] & 0x0f {
402                0 => json!({"ipv4": ip4(&v[1..])?.to_string()}),
403                1 => json!({"ipv6": ip6(&v[1..])?.to_string()}),
404                2 => json!({"fqdn": text(&v[1..])}),
405                other => bail!("Node ID type {other}"),
406            }
407        }
408        K::FSeid => {
409            need(9)?;
410            let mut o = json!({"seid": u64::from_be_bytes(v[1..9].try_into()?)});
411            let mut at = 9;
412            if v[0] & 0x02 != 0 {
413                o["ipv4"] = json!(ip4(&v[at..])?.to_string());
414                at += 4;
415            }
416            if v[0] & 0x01 != 0 {
417                o["ipv6"] = json!(ip6(&v[at..])?.to_string());
418            }
419            o
420        }
421        K::FTeid => {
422            need(1)?;
423            let f = v[0];
424            if f & 0x04 != 0 {
425                // CH: "choose one for me"; with CHID, one more byte follows.
426                let mut o = json!({"choose": true, "ipv4": f & 0x01 != 0, "ipv6": f & 0x02 != 0});
427                if f & 0x08 != 0 {
428                    need(2)?;
429                    o["choose_id"] = json!(v[1]);
430                }
431                o
432            } else {
433                need(5)?;
434                let mut o = json!({"teid": u32::from_be_bytes(v[1..5].try_into()?)});
435                let mut at = 5;
436                if f & 0x01 != 0 {
437                    o["ipv4"] = json!(ip4(&v[at..])?.to_string());
438                    at += 4;
439                }
440                if f & 0x02 != 0 {
441                    o["ipv6"] = json!(ip6(&v[at..])?.to_string());
442                }
443                o
444            }
445        }
446        K::UeIp => {
447            need(1)?;
448            let f = v[0];
449            let mut o = json!({"direction": if f & 0x04 != 0 { "destination" } else { "source" }});
450            let mut at = 1;
451            if f & 0x02 != 0 {
452                o["ipv4"] = json!(ip4(&v[at..])?.to_string());
453                at += 4;
454            }
455            if f & 0x01 != 0 {
456                o["ipv6"] = json!(ip6(&v[at..])?.to_string());
457            }
458            o
459        }
460        K::ApplyAction => {
461            need(1)?;
462            let bits = u32::from(v[0]) | v.get(1).map_or(0, |b| u32::from(*b) << 8);
463            json!(flags(bits, APPLY))
464        }
465        K::Interface => {
466            need(1)?;
467            let i = (v[0] & 0x0f) as usize;
468            json!(INTERFACES
469                .get(i)
470                .map(|s| s.to_string())
471                .unwrap_or_else(|| format!("interface_{i}")))
472        }
473        K::Text => json!(text(v)),
474        K::Recovery => {
475            need(4)?;
476            let ntp = u64::from(u32::from_be_bytes(v[..4].try_into()?));
477            json!(ntp.saturating_sub(NTP_UNIX))
478        }
479        K::OuterHeaderCreation => {
480            need(2)?;
481            // Octet 5 is the description (bit 1 GTP-U/UDP/IPv4 … bit 8 S-TAG); octet 6 holds
482            // the N19/N6 indications.
483            let d = v[0];
484            let mut o = json!({"description": flags(u32::from(d), OHC)});
485            let mut at = 2;
486            if d & 0x03 != 0 {
487                need(at + 4)?;
488                o["teid"] = json!(u32::from_be_bytes(v[at..at + 4].try_into()?));
489                at += 4;
490            }
491            if d & 0x15 != 0 {
492                o["ipv4"] = json!(ip4(&v[at..])?.to_string());
493                at += 4;
494            }
495            if d & 0x2a != 0 {
496                o["ipv6"] = json!(ip6(&v[at..])?.to_string());
497                at += 16;
498            }
499            if d & 0x0c != 0 {
500                need(at + 2)?;
501                o["port"] = json!(u16::from_be_bytes([v[at], v[at + 1]]));
502            }
503            o
504        }
505        K::OuterHeaderRemoval => {
506            need(1)?;
507            json!(v[0])
508        }
509        K::Hex => json!({"hex": hex::encode(v)}),
510    })
511}
512
513/// Encode a message: header, then the IEs of `ies` (the model's form).
514pub fn encode(message_type: u8, seid: Option<u64>, sequence: u32, ies: &Value) -> Result<Vec<u8>> {
515    let mut body = Vec::new();
516    encode_ies(ies, &mut body, 0)?;
517    let mut out = Vec::with_capacity(16 + body.len());
518    out.push((1 << 5) | u8::from(seid.is_some()));
519    out.push(message_type);
520    let len = body.len() + 4 + if seid.is_some() { 8 } else { 0 };
521    ensure!(
522        len <= u16::MAX as usize,
523        "a {len}-byte message is too long for PFCP"
524    );
525    out.extend_from_slice(&(len as u16).to_be_bytes());
526    if let Some(s) = seid {
527        out.extend_from_slice(&s.to_be_bytes());
528    }
529    out.extend_from_slice(&sequence.to_be_bytes()[1..]);
530    out.push(0);
531    out.extend_from_slice(&body);
532    ensure!(
533        out.len() <= MAX_DATAGRAM,
534        "the message is too large for one datagram"
535    );
536    Ok(out)
537}
538
539fn encode_ies(ies: &Value, out: &mut Vec<u8>, depth: usize) -> Result<()> {
540    ensure!(
541        depth < MAX_DEPTH,
542        "grouped IEs nest more than {MAX_DEPTH} deep"
543    );
544    let map = match ies {
545        Value::Object(m) => m,
546        Value::Null => return Ok(()),
547        _ => bail!("IEs are an object keyed by IE name"),
548    };
549    for (name, value) in map {
550        let (t, kind) = by_name(name)?;
551        // An array is the IE repeated, except apply_action, whose own value is a list of
552        // flag names (a list of such lists is the repetition).
553        let repeated = match (kind, value) {
554            (K::ApplyAction, Value::Array(list)) => list.iter().all(Value::is_array),
555            (_, Value::Array(_)) => true,
556            _ => false,
557        };
558        let items: Vec<&Value> = match value {
559            Value::Array(list) if repeated => list.iter().collect(),
560            other => vec![other],
561        };
562        for item in items {
563            let mut v = Vec::new();
564            encode_value(kind, item, &mut v, depth).with_context(|| format!("IE {name}"))?;
565            ensure!(v.len() <= u16::MAX as usize, "IE {name} is too long");
566            out.extend_from_slice(&t.to_be_bytes());
567            out.extend_from_slice(&(v.len() as u16).to_be_bytes());
568            out.extend_from_slice(&v);
569        }
570    }
571    Ok(())
572}
573
574fn num(v: &Value) -> Result<u64> {
575    v.as_u64().context("a whole number")
576}
577
578fn parse_ip4(v: &Value) -> Result<Ipv4Addr> {
579    v.as_str()
580        .context("an IPv4 address string")?
581        .parse()
582        .context("an IPv4 address")
583}
584
585fn parse_ip6(v: &Value) -> Result<Ipv6Addr> {
586    v.as_str()
587        .context("an IPv6 address string")?
588        .parse()
589        .context("an IPv6 address")
590}
591
592fn flag_bits(v: &Value, names: &[&str]) -> Result<u32> {
593    let list: Vec<&str> = match v {
594        Value::String(s) => vec![s.as_str()],
595        Value::Array(a) => a
596            .iter()
597            .map(|x| x.as_str().context("flag names are strings"))
598            .collect::<Result<_>>()?,
599        _ => bail!("one of {} or a list of them", names.join(", ")),
600    };
601    let mut bits = 0;
602    for f in list {
603        let i = names
604            .iter()
605            .position(|n| n.eq_ignore_ascii_case(f))
606            .with_context(|| format!("{f:?} is not one of {}", names.join(", ")))?;
607        bits |= 1 << i;
608    }
609    Ok(bits)
610}
611
612fn encode_value(kind: K, v: &Value, out: &mut Vec<u8>, depth: usize) -> Result<()> {
613    match kind {
614        K::Grouped => encode_ies(v, out, depth + 1)?,
615        K::U8 => out.push(u8::try_from(num(v)?).context("0-255")?),
616        K::U16 => out.extend_from_slice(&u16::try_from(num(v)?).context("0-65535")?.to_be_bytes()),
617        K::U32 => out.extend_from_slice(
618            &u32::try_from(num(v)?)
619                .context("a 32-bit number")?
620                .to_be_bytes(),
621        ),
622        K::Cause => {
623            let c = match v {
624                Value::String(s) => cause_code(s)?,
625                n => u8::try_from(num(n)?).context("0-255")?,
626            };
627            out.push(c);
628        }
629        K::NodeId => {
630            if let Some(a) = v.get("ipv4") {
631                out.push(0);
632                out.extend_from_slice(&parse_ip4(a)?.octets());
633            } else if let Some(a) = v.get("ipv6") {
634                out.push(1);
635                out.extend_from_slice(&parse_ip6(a)?.octets());
636            } else if let Some(f) = v.get("fqdn").and_then(Value::as_str) {
637                out.push(2);
638                for label in f.split('.').filter(|l| !l.is_empty()) {
639                    ensure!(label.len() < 64, "an FQDN label is longer than 63 bytes");
640                    out.push(label.len() as u8);
641                    out.extend_from_slice(label.as_bytes());
642                }
643            } else {
644                bail!("node_id is {{\"ipv4\": …}}, {{\"ipv6\": …}} or {{\"fqdn\": …}}");
645            }
646        }
647        K::FSeid => {
648            let (v4, v6) = (v.get("ipv4"), v.get("ipv6"));
649            out.push(u8::from(v4.is_some()) << 1 | u8::from(v6.is_some()));
650            out.extend_from_slice(&num(v.get("seid").context("f_seid needs seid")?)?.to_be_bytes());
651            if let Some(a) = v4 {
652                out.extend_from_slice(&parse_ip4(a)?.octets());
653            }
654            if let Some(a) = v6 {
655                out.extend_from_slice(&parse_ip6(a)?.octets());
656            }
657        }
658        K::FTeid => {
659            if v.get("choose").and_then(Value::as_bool) == Some(true) {
660                let v4 = v.get("ipv4").and_then(Value::as_bool).unwrap_or(true);
661                let v6 = v.get("ipv6").and_then(Value::as_bool).unwrap_or(false);
662                let chid = v.get("choose_id").map(num).transpose()?;
663                out.push(0x04 | u8::from(v4) | u8::from(v6) << 1 | u8::from(chid.is_some()) << 3);
664                if let Some(id) = chid {
665                    out.push(u8::try_from(id).context("choose_id is 0-255")?);
666                }
667            } else {
668                let (v4, v6) = (v.get("ipv4"), v.get("ipv6"));
669                out.push(u8::from(v4.is_some()) | u8::from(v6.is_some()) << 1);
670                out.extend_from_slice(
671                    &u32::try_from(num(v
672                        .get("teid")
673                        .context("f_teid needs teid (or choose: true)")?)?)?
674                    .to_be_bytes(),
675                );
676                if let Some(a) = v4 {
677                    out.extend_from_slice(&parse_ip4(a)?.octets());
678                }
679                if let Some(a) = v6 {
680                    out.extend_from_slice(&parse_ip6(a)?.octets());
681                }
682            }
683        }
684        K::UeIp => {
685            let (v4, v6) = (v.get("ipv4"), v.get("ipv6"));
686            let dest = v.get("direction").and_then(Value::as_str) == Some("destination");
687            out.push(u8::from(dest) << 2 | u8::from(v4.is_some()) << 1 | u8::from(v6.is_some()));
688            if let Some(a) = v4 {
689                out.extend_from_slice(&parse_ip4(a)?.octets());
690            }
691            if let Some(a) = v6 {
692                out.extend_from_slice(&parse_ip6(a)?.octets());
693            }
694        }
695        K::ApplyAction => {
696            let bits = flag_bits(v, APPLY)?;
697            out.push(bits as u8);
698            if bits > 0xff {
699                out.push((bits >> 8) as u8);
700            }
701        }
702        K::Interface => {
703            let i = match v {
704                Value::String(s) => INTERFACES.iter().position(|n| n == s).with_context(|| {
705                    format!("interface {s:?} is not one of {}", INTERFACES.join(", "))
706                })?,
707                n => num(n)? as usize,
708            };
709            out.push(u8::try_from(i).context("an interface number")? & 0x0f);
710        }
711        K::Text => out.extend_from_slice(v.as_str().context("text")?.as_bytes()),
712        K::Recovery => {
713            let unix = num(v)?;
714            out.extend_from_slice(&((unix + NTP_UNIX) as u32).to_be_bytes());
715        }
716        K::OuterHeaderCreation => {
717            let d = flag_bits(
718                v.get("description")
719                    .context("outer_header_creation needs description")?,
720                OHC,
721            )? as u8;
722            ensure!(
723                d & 0xc0 == 0,
724                "C-TAG and S-TAG outer headers are not supported"
725            );
726            out.extend_from_slice(&[d, 0]);
727            if d & 0x03 != 0 {
728                out.extend_from_slice(
729                    &u32::try_from(num(v.get("teid").context("GTP-U needs teid")?)?)?.to_be_bytes(),
730                );
731            }
732            if d & 0x15 != 0 {
733                out.extend_from_slice(
734                    &parse_ip4(v.get("ipv4").context("an IPv4 outer header needs ipv4")?)?.octets(),
735                );
736            }
737            if d & 0x2a != 0 {
738                out.extend_from_slice(
739                    &parse_ip6(v.get("ipv6").context("an IPv6 outer header needs ipv6")?)?.octets(),
740                );
741            }
742            if d & 0x0c != 0 {
743                out.extend_from_slice(
744                    &u16::try_from(num(v.get("port").context("UDP needs port")?)?)?.to_be_bytes(),
745                );
746            }
747        }
748        K::OuterHeaderRemoval => out.push(u8::try_from(num(v)?).context("0-255")?),
749        K::Hex => {
750            let h = v
751                .get("hex")
752                .and_then(Value::as_str)
753                .or(v.as_str())
754                .context("give this IE as {\"hex\": \"…\"}")?;
755            out.extend(hex::decode(h).context("not hex")?);
756        }
757    }
758    Ok(())
759}
760
761/// The current time as a recovery time stamp's unix seconds.
762pub fn now_unix() -> u64 {
763    crate::utils::clock::SystemTime::now()
764        .duration_since(crate::utils::clock::UNIX_EPOCH)
765        .map(|d| d.as_secs())
766        .unwrap_or(0)
767}