ftth_rtnl/
virtual_interface.rs

1#![allow(unreachable_patterns)]
2
3use std::io::{self, ErrorKind};
4use std::net::{Ipv4Addr, Ipv6Addr};
5
6use ftth_common::channel::{AsyncWorldClient, AsyncWorldServer};
7use futures::TryStreamExt;
8use netlink_packet_core::{DefaultNla, Nla};
9use netlink_packet_route::link::{
10    InfoData, InfoGreTap, InfoGreTap6, InfoGreTun, InfoGreTun6, InfoKind, InfoVlan, LinkMessage,
11};
12use rtnetlink::{LinkMessageBuilder, LinkUnspec};
13
14pub(crate) type Client =
15    AsyncWorldClient<RtnlVirtualInterfaceRequest, RtnlVirtualInterfaceResponse>;
16pub(crate) type Server =
17    AsyncWorldServer<RtnlVirtualInterfaceRequest, RtnlVirtualInterfaceResponse>;
18
19#[derive(Debug, Clone, PartialEq)]
20#[non_exhaustive]
21pub enum RtnlVirtualInterfaceRequest {
22    Create(VirtualInterfaceSpec),
23    Configure(VirtualInterfaceUpdate),
24    Delete(VirtualInterfaceDelete),
25    GetIndexByName(String),
26}
27
28#[derive(Debug, Clone, PartialEq)]
29#[non_exhaustive]
30pub enum RtnlVirtualInterfaceResponse {
31    Success,
32    Failed,
33    NotFound,
34    Index(u32),
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct RtnlVirtualInterfaceClient {
39    client: Client,
40}
41
42impl RtnlVirtualInterfaceClient {
43    pub(crate) fn new(client: Client) -> Self {
44        Self { client }
45    }
46
47    pub fn create(&self, spec: VirtualInterfaceSpec) -> io::Result<()> {
48        let res = self
49            .client
50            .send_request(RtnlVirtualInterfaceRequest::Create(spec))?;
51        handle_basic_response("Create virtual interface", res)
52    }
53
54    pub fn configure(&self, update: VirtualInterfaceUpdate) -> io::Result<()> {
55        let res = self
56            .client
57            .send_request(RtnlVirtualInterfaceRequest::Configure(update))?;
58        handle_basic_response("Configure virtual interface", res)
59    }
60
61    pub fn delete(&self, delete: VirtualInterfaceDelete) -> io::Result<()> {
62        let res = self
63            .client
64            .send_request(RtnlVirtualInterfaceRequest::Delete(delete))?;
65        handle_basic_response("Delete virtual interface", res)
66    }
67
68    pub fn get_index_by_name(&self, name: &str) -> io::Result<u32> {
69        match self
70            .client
71            .send_request(RtnlVirtualInterfaceRequest::GetIndexByName(
72                name.to_string(),
73            ))? {
74            RtnlVirtualInterfaceResponse::Index(index) => Ok(index),
75            RtnlVirtualInterfaceResponse::NotFound => Err(io::Error::new(
76                ErrorKind::NotFound,
77                format!("Virtual interface {name} not found"),
78            )),
79            other => Err(io::Error::other(format!(
80                "Unexpected response while fetching index: {:?}",
81                other
82            ))),
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct VirtualInterfaceSpec {
89    pub name: String,
90    pub kind: VirtualInterfaceKind,
91    pub admin_up: bool,
92}
93
94#[derive(Debug, Clone, PartialEq)]
95pub struct VirtualInterfaceUpdate {
96    pub if_id: u32,
97    pub new_name: Option<String>,
98    pub kind: VirtualInterfaceKind,
99    pub admin_up: Option<bool>,
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub enum VirtualInterfaceDelete {
104    ByIndex(u32),
105    ByName(String),
106}
107
108#[derive(Debug, Clone, PartialEq)]
109pub enum VirtualInterfaceKind {
110    Gre(GreConfig),
111    Gretap(GreConfig),
112    Ip6Gre(Gre6Config),
113    Ip6Gretap(Gre6Config),
114    IpIp(IpIpConfig),
115    Ip6Tnl(Ip6TnlConfig),
116    Vlan(VlanConfig),
117}
118
119#[derive(Debug, Clone, PartialEq)]
120pub struct GreConfig {
121    pub local: Ipv4Addr,
122    pub remote: Ipv4Addr,
123    pub ttl: Option<u8>,
124    pub tos: Option<u8>,
125    pub key: Option<u32>,
126    pub encap_limit: Option<u8>,
127    pub pmtudisc: bool,
128    pub ignore_df: bool,
129    pub link: Option<u32>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct Gre6Config {
134    pub local: Ipv6Addr,
135    pub remote: Ipv6Addr,
136    pub hop_limit: Option<u8>,
137    pub traffic_class: Option<u8>,
138    pub key: Option<u32>,
139    pub encap_limit: Option<u8>,
140    pub pmtudisc: bool,
141    pub ignore_df: bool,
142    pub link: Option<u32>,
143}
144
145#[derive(Debug, Clone, PartialEq)]
146pub struct IpIpConfig {
147    pub local: Ipv4Addr,
148    pub remote: Ipv4Addr,
149    pub ttl: Option<u8>,
150    pub tos: Option<u8>,
151    pub encap_limit: Option<u8>,
152    pub pmtudisc: bool,
153    pub link: Option<u32>,
154}
155
156#[derive(Debug, Clone, PartialEq)]
157pub struct Ip6TnlConfig {
158    pub local: Ipv6Addr,
159    pub remote: Ipv6Addr,
160    pub hop_limit: Option<u8>,
161    pub traffic_class: Option<u8>,
162    pub flow_label: Option<u32>,
163    pub encap_limit: Option<u8>,
164    pub pmtudisc: bool,
165    pub link: Option<u32>,
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub struct VlanConfig {
170    pub base_ifindex: Option<u32>,
171    pub vlan_id: Option<u16>,
172}
173
174const IFLA_GRE_LINK: u16 = 1;
175const IFLA_GRE_IKEY: u16 = 4;
176const IFLA_GRE_OKEY: u16 = 5;
177const IFLA_GRE_LOCAL: u16 = 6;
178const IFLA_GRE_REMOTE: u16 = 7;
179const IFLA_GRE_TTL: u16 = 8;
180const IFLA_GRE_TOS: u16 = 9;
181const IFLA_GRE_PMTUDISC: u16 = 10;
182const IFLA_GRE_ENCAP_LIMIT: u16 = 11;
183const IFLA_GRE_IGNORE_DF: u16 = 19;
184
185const IFLA_IPTUN_LINK: u16 = 1;
186const IFLA_IPTUN_LOCAL: u16 = 2;
187const IFLA_IPTUN_REMOTE: u16 = 3;
188const IFLA_IPTUN_TTL: u16 = 4;
189const IFLA_IPTUN_TOS: u16 = 5;
190const IFLA_IPTUN_ENCAP_LIMIT: u16 = 6;
191const IFLA_IPTUN_FLOWINFO: u16 = 7;
192const IFLA_IPTUN_PMTUDISC: u16 = 10;
193
194const NLA_HEADER_LEN: usize = 4;
195const NLA_ALIGNTO: usize = 4;
196
197fn align_nla(len: usize) -> usize {
198    (len + NLA_ALIGNTO - 1) & !(NLA_ALIGNTO - 1)
199}
200
201fn handle_basic_response(op: &str, response: RtnlVirtualInterfaceResponse) -> io::Result<()> {
202    match response {
203        RtnlVirtualInterfaceResponse::Success => Ok(()),
204        RtnlVirtualInterfaceResponse::NotFound => Err(io::Error::new(
205            ErrorKind::NotFound,
206            format!("{}: target not found", op),
207        )),
208        RtnlVirtualInterfaceResponse::Failed => Err(io::Error::other(format!("{} failed", op))),
209        other => Err(io::Error::other(format!(
210            "{} returned unexpected response: {:?}",
211            op, other
212        ))),
213    }
214}
215
216pub(crate) async fn run_server(mut server: Server, mut handle: rtnetlink::LinkHandle) {
217    while let Some((req, respond)) = server.accept().await {
218        match req {
219            RtnlVirtualInterfaceRequest::Create(spec) => {
220                let message = match build_create_message(&spec) {
221                    Ok(msg) => msg,
222                    Err(err) => {
223                        log::warn!("Failed to build virtual interface {}: {}", spec.name, err);
224                        respond(RtnlVirtualInterfaceResponse::Failed);
225                        continue;
226                    }
227                };
228
229                match handle.add(message).execute().await {
230                    Ok(()) => respond(RtnlVirtualInterfaceResponse::Success),
231                    Err(rtnetlink::Error::NetlinkError(err_msg)) => {
232                        log::warn!(
233                            "Netlink error creating virtual interface {}: {}",
234                            spec.name,
235                            err_msg
236                        );
237                        respond(netlink_error_to_response(err_msg.to_io()));
238                    }
239                    Err(err) => {
240                        log::warn!("Failed to create virtual interface {}: {}", spec.name, err);
241                        respond(RtnlVirtualInterfaceResponse::Failed);
242                    }
243                }
244            }
245            RtnlVirtualInterfaceRequest::Configure(update) => {
246                let message = match build_update_message(&update) {
247                    Ok(msg) => msg,
248                    Err(err) => {
249                        log::warn!(
250                            "Failed to build virtual interface update for {}: {}",
251                            update.if_id,
252                            err
253                        );
254                        respond(RtnlVirtualInterfaceResponse::Failed);
255                        continue;
256                    }
257                };
258
259                match handle.set(message).execute().await {
260                    Ok(()) => respond(RtnlVirtualInterfaceResponse::Success),
261                    Err(rtnetlink::Error::NetlinkError(err_msg)) => {
262                        respond(netlink_error_to_response(err_msg.to_io()));
263                    }
264                    Err(err) => {
265                        log::warn!(
266                            "Failed to configure virtual interface {}: {}",
267                            update.if_id,
268                            err
269                        );
270                        respond(RtnlVirtualInterfaceResponse::Failed);
271                    }
272                }
273            }
274            RtnlVirtualInterfaceRequest::Delete(delete) => {
275                let result = match resolve_delete_target(&mut handle, &delete).await {
276                    Ok(index) => handle.del(index).execute().await.map_err(|err| match err {
277                        rtnetlink::Error::NetlinkError(e) => e.to_io(),
278                        other => io::Error::other(other.to_string()),
279                    }),
280                    Err(err) => Err(err),
281                };
282
283                match result {
284                    Ok(()) => respond(RtnlVirtualInterfaceResponse::Success),
285                    Err(err) if err.kind() == ErrorKind::NotFound => {
286                        respond(RtnlVirtualInterfaceResponse::NotFound)
287                    }
288                    Err(err) => {
289                        log::warn!("Failed to delete virtual interface: {}", err);
290                        respond(RtnlVirtualInterfaceResponse::Failed);
291                    }
292                }
293            }
294            RtnlVirtualInterfaceRequest::GetIndexByName(name) => {
295                match resolve_index_by_name(&mut handle, &name).await {
296                    Ok(Some(index)) => respond(RtnlVirtualInterfaceResponse::Index(index)),
297                    Ok(None) => respond(RtnlVirtualInterfaceResponse::NotFound),
298                    Err(err) => {
299                        log::warn!("Failed to resolve virtual interface {}: {}", name, err);
300                        respond(RtnlVirtualInterfaceResponse::Failed);
301                    }
302                }
303            }
304        }
305    }
306}
307
308fn netlink_error_to_response(err: io::Error) -> RtnlVirtualInterfaceResponse {
309    match err.kind() {
310        ErrorKind::NotFound => RtnlVirtualInterfaceResponse::NotFound,
311        _ => RtnlVirtualInterfaceResponse::Failed,
312    }
313}
314
315fn build_create_message(spec: &VirtualInterfaceSpec) -> io::Result<LinkMessage> {
316    validate_create_kind(&spec.kind)?;
317    let info_kind = virtual_interface_kind_to_info_kind(&spec.kind);
318    let mut builder = LinkMessageBuilder::<LinkUnspec>::new_with_info_kind(info_kind)
319        .name(spec.name.clone())
320        .set_info_data(build_info_data(&spec.kind)?);
321
322    if spec.admin_up {
323        builder = builder.up();
324    }
325
326    if let Some(link) = virtual_interface_link(&spec.kind) {
327        builder = builder.link(link);
328    }
329
330    Ok(builder.build())
331}
332
333fn validate_create_kind(kind: &VirtualInterfaceKind) -> io::Result<()> {
334    match kind {
335        VirtualInterfaceKind::Vlan(cfg) => {
336            if cfg.base_ifindex.is_none() {
337                return Err(io::Error::other(
338                    "VLAN creation requires a parent interface (--dev)",
339                ));
340            }
341            if cfg.vlan_id.is_none() {
342                return Err(io::Error::other("VLAN creation requires --vlan-id"));
343            }
344            Ok(())
345        }
346        _ => Ok(()),
347    }
348}
349
350fn build_update_message(update: &VirtualInterfaceUpdate) -> io::Result<LinkMessage> {
351    let info_kind = virtual_interface_kind_to_info_kind(&update.kind);
352    let mut builder = LinkMessageBuilder::<LinkUnspec>::new_with_info_kind(info_kind)
353        .index(update.if_id)
354        .set_info_data(build_info_data(&update.kind)?);
355
356    if let Some(name) = &update.new_name {
357        builder = builder.name(name.clone());
358    }
359
360    if let Some(link) = virtual_interface_link(&update.kind) {
361        builder = builder.link(link);
362    }
363
364    if let Some(up) = update.admin_up {
365        builder = if up { builder.up() } else { builder.down() };
366    }
367
368    Ok(builder.build())
369}
370
371async fn resolve_delete_target(
372    handle: &mut rtnetlink::LinkHandle,
373    delete: &VirtualInterfaceDelete,
374) -> io::Result<u32> {
375    match delete {
376        VirtualInterfaceDelete::ByIndex(index) => Ok(*index),
377        VirtualInterfaceDelete::ByName(name) => resolve_index_by_name(handle, name)
378            .await?
379            .ok_or_else(|| io::Error::new(ErrorKind::NotFound, "Virtual interface not found")),
380    }
381}
382
383async fn resolve_index_by_name(
384    handle: &mut rtnetlink::LinkHandle,
385    name: &str,
386) -> io::Result<Option<u32>> {
387    let response = handle.get().match_name(name.to_string()).execute();
388    futures::pin_mut!(response);
389    while let Ok(Some(msg)) = response.try_next().await {
390        if msg.header.index != 0 {
391            return Ok(Some(msg.header.index));
392        }
393    }
394    Ok(None)
395}
396
397fn virtual_interface_kind_to_info_kind(kind: &VirtualInterfaceKind) -> InfoKind {
398    match kind {
399        VirtualInterfaceKind::Gre(_) => InfoKind::GreTun,
400        VirtualInterfaceKind::Gretap(_) => InfoKind::GreTap,
401        VirtualInterfaceKind::Ip6Gre(_) => InfoKind::GreTun6,
402        VirtualInterfaceKind::Ip6Gretap(_) => InfoKind::GreTap6,
403        VirtualInterfaceKind::IpIp(_) => InfoKind::IpTun,
404        VirtualInterfaceKind::Ip6Tnl(_) => InfoKind::Other("ip6tnl".into()),
405        VirtualInterfaceKind::Vlan(_) => InfoKind::Vlan,
406    }
407}
408
409fn virtual_interface_link(kind: &VirtualInterfaceKind) -> Option<u32> {
410    match kind {
411        VirtualInterfaceKind::Gre(cfg) | VirtualInterfaceKind::Gretap(cfg) => cfg.link,
412        VirtualInterfaceKind::Ip6Gre(cfg) | VirtualInterfaceKind::Ip6Gretap(cfg) => cfg.link,
413        VirtualInterfaceKind::IpIp(cfg) => cfg.link,
414        VirtualInterfaceKind::Ip6Tnl(cfg) => cfg.link,
415        VirtualInterfaceKind::Vlan(cfg) => cfg.base_ifindex,
416    }
417}
418
419fn build_info_data(kind: &VirtualInterfaceKind) -> io::Result<InfoData> {
420    match kind {
421        VirtualInterfaceKind::Gre(cfg) => Ok(InfoData::GreTun(
422            gre_nlas(cfg).into_iter().map(InfoGreTun::Other).collect(),
423        )),
424        VirtualInterfaceKind::Gretap(cfg) => Ok(InfoData::GreTap(
425            gre_nlas(cfg).into_iter().map(InfoGreTap::Other).collect(),
426        )),
427        VirtualInterfaceKind::Ip6Gre(cfg) => Ok(InfoData::GreTun6(
428            gre6_nlas(cfg).into_iter().map(InfoGreTun6::Other).collect(),
429        )),
430        VirtualInterfaceKind::Ip6Gretap(cfg) => Ok(InfoData::GreTap6(
431            gre6_nlas(cfg).into_iter().map(InfoGreTap6::Other).collect(),
432        )),
433        VirtualInterfaceKind::IpIp(cfg) => {
434            let nlas = iptunnel_v4_nlas(cfg);
435            Ok(InfoData::Other(encode_default_nlas(&nlas)))
436        }
437        VirtualInterfaceKind::Ip6Tnl(cfg) => {
438            let nlas = iptunnel_v6_nlas(cfg);
439            Ok(InfoData::Other(encode_default_nlas(&nlas)))
440        }
441        VirtualInterfaceKind::Vlan(cfg) => {
442            let mut infos = Vec::new();
443            if let Some(id) = cfg.vlan_id {
444                infos.push(InfoVlan::Id(id));
445            }
446            Ok(InfoData::Vlan(infos))
447        }
448    }
449}
450
451fn gre_nlas(cfg: &GreConfig) -> Vec<DefaultNla> {
452    let mut nlas = Vec::new();
453    nlas.push(DefaultNla::new(IFLA_GRE_LOCAL, cfg.local.octets().to_vec()));
454    nlas.push(DefaultNla::new(
455        IFLA_GRE_REMOTE,
456        cfg.remote.octets().to_vec(),
457    ));
458
459    if let Some(ttl) = cfg.ttl {
460        nlas.push(DefaultNla::new(IFLA_GRE_TTL, vec![ttl]));
461    }
462
463    if let Some(tos) = cfg.tos {
464        nlas.push(DefaultNla::new(IFLA_GRE_TOS, vec![tos]));
465    }
466
467    if let Some(key) = cfg.key {
468        let bytes = key.to_be_bytes().to_vec();
469        nlas.push(DefaultNla::new(IFLA_GRE_IKEY, bytes.clone()));
470        nlas.push(DefaultNla::new(IFLA_GRE_OKEY, bytes));
471    }
472
473    let limit = cfg.encap_limit.unwrap_or(0xff);
474    nlas.push(DefaultNla::new(IFLA_GRE_ENCAP_LIMIT, vec![limit]));
475
476    nlas.push(DefaultNla::new(
477        IFLA_GRE_PMTUDISC,
478        vec![if cfg.pmtudisc { 1 } else { 0 }],
479    ));
480
481    nlas.push(DefaultNla::new(
482        IFLA_GRE_IGNORE_DF,
483        vec![if cfg.ignore_df { 1 } else { 0 }],
484    ));
485
486    if let Some(link) = cfg.link {
487        nlas.push(DefaultNla::new(IFLA_GRE_LINK, link.to_ne_bytes().to_vec()));
488    }
489
490    nlas
491}
492
493fn gre6_nlas(cfg: &Gre6Config) -> Vec<DefaultNla> {
494    let mut nlas = Vec::new();
495    nlas.push(DefaultNla::new(IFLA_GRE_LOCAL, cfg.local.octets().to_vec()));
496    nlas.push(DefaultNla::new(
497        IFLA_GRE_REMOTE,
498        cfg.remote.octets().to_vec(),
499    ));
500
501    if let Some(hop) = cfg.hop_limit {
502        nlas.push(DefaultNla::new(IFLA_GRE_TTL, vec![hop]));
503    }
504
505    if let Some(tc) = cfg.traffic_class {
506        nlas.push(DefaultNla::new(IFLA_GRE_TOS, vec![tc]));
507    }
508
509    if let Some(key) = cfg.key {
510        let bytes = key.to_be_bytes().to_vec();
511        nlas.push(DefaultNla::new(IFLA_GRE_IKEY, bytes.clone()));
512        nlas.push(DefaultNla::new(IFLA_GRE_OKEY, bytes));
513    }
514
515    let limit = cfg.encap_limit.unwrap_or(0xff);
516    nlas.push(DefaultNla::new(IFLA_GRE_ENCAP_LIMIT, vec![limit]));
517
518    nlas.push(DefaultNla::new(
519        IFLA_GRE_PMTUDISC,
520        vec![if cfg.pmtudisc { 1 } else { 0 }],
521    ));
522
523    nlas.push(DefaultNla::new(
524        IFLA_GRE_IGNORE_DF,
525        vec![if cfg.ignore_df { 1 } else { 0 }],
526    ));
527
528    if let Some(link) = cfg.link {
529        nlas.push(DefaultNla::new(IFLA_GRE_LINK, link.to_ne_bytes().to_vec()));
530    }
531
532    nlas
533}
534
535fn iptunnel_v4_nlas(cfg: &IpIpConfig) -> Vec<DefaultNla> {
536    let mut nlas = Vec::new();
537    nlas.push(DefaultNla::new(
538        IFLA_IPTUN_LOCAL,
539        cfg.local.octets().to_vec(),
540    ));
541    nlas.push(DefaultNla::new(
542        IFLA_IPTUN_REMOTE,
543        cfg.remote.octets().to_vec(),
544    ));
545
546    if let Some(ttl) = cfg.ttl {
547        nlas.push(DefaultNla::new(IFLA_IPTUN_TTL, vec![ttl]));
548    }
549
550    if let Some(tos) = cfg.tos {
551        nlas.push(DefaultNla::new(IFLA_IPTUN_TOS, vec![tos]));
552    }
553
554    let limit = cfg.encap_limit.unwrap_or(0xff);
555    nlas.push(DefaultNla::new(IFLA_IPTUN_ENCAP_LIMIT, vec![limit]));
556
557    nlas.push(DefaultNla::new(
558        IFLA_IPTUN_PMTUDISC,
559        vec![if cfg.pmtudisc { 1 } else { 0 }],
560    ));
561
562    if let Some(link) = cfg.link {
563        nlas.push(DefaultNla::new(
564            IFLA_IPTUN_LINK,
565            link.to_ne_bytes().to_vec(),
566        ));
567    }
568
569    nlas
570}
571
572fn iptunnel_v6_nlas(cfg: &Ip6TnlConfig) -> Vec<DefaultNla> {
573    let mut nlas = Vec::new();
574    nlas.push(DefaultNla::new(
575        IFLA_IPTUN_LOCAL,
576        cfg.local.octets().to_vec(),
577    ));
578    nlas.push(DefaultNla::new(
579        IFLA_IPTUN_REMOTE,
580        cfg.remote.octets().to_vec(),
581    ));
582
583    if let Some(hop) = cfg.hop_limit {
584        nlas.push(DefaultNla::new(IFLA_IPTUN_TTL, vec![hop]));
585    }
586
587    if let Some(tc) = cfg.traffic_class {
588        nlas.push(DefaultNla::new(IFLA_IPTUN_TOS, vec![tc]));
589    }
590
591    if let Some(flow) = cfg.flow_label {
592        nlas.push(DefaultNla::new(
593            IFLA_IPTUN_FLOWINFO,
594            flow.to_be_bytes().to_vec(),
595        ));
596    }
597
598    let limit = cfg.encap_limit.unwrap_or(0xff);
599    nlas.push(DefaultNla::new(IFLA_IPTUN_ENCAP_LIMIT, vec![limit]));
600
601    nlas.push(DefaultNla::new(
602        IFLA_IPTUN_PMTUDISC,
603        vec![if cfg.pmtudisc { 1 } else { 0 }],
604    ));
605
606    if let Some(link) = cfg.link {
607        nlas.push(DefaultNla::new(
608            IFLA_IPTUN_LINK,
609            link.to_ne_bytes().to_vec(),
610        ));
611    }
612
613    nlas
614}
615
616fn encode_default_nlas(nlas: &[DefaultNla]) -> Vec<u8> {
617    let mut buffer = Vec::new();
618    for nla in nlas {
619        let value_len = nla.value_len();
620        let payload_len = value_len + NLA_HEADER_LEN;
621        let aligned_len = align_nla(payload_len);
622        let start = buffer.len();
623        buffer.resize(start + aligned_len, 0);
624        let target = &mut buffer[start..start + aligned_len];
625        let len_bytes = (payload_len as u16).to_ne_bytes();
626        target[0..2].copy_from_slice(&len_bytes);
627        let kind_bytes = (nla.kind() as u16).to_ne_bytes();
628        target[2..4].copy_from_slice(&kind_bytes);
629        nla.emit_value(&mut target[4..payload_len]);
630        // trailing padding already zeroed by resize
631    }
632    buffer
633}