libnet/
ip.rs

1// Copyright 2024 Oxide Computer Company
2
3use crate::nvlist::{NvDataType, NvHeader, NvPair, Value, NVP};
4use libc::{free, malloc, sockaddr_in6};
5use rusty_doors::{door_call, door_callp};
6use std::collections::HashMap;
7use std::convert::TryInto;
8use std::ffi::CStr;
9use std::fs::File;
10use std::mem::{align_of, size_of};
11use std::net::{Ipv4Addr, Ipv6Addr};
12use std::os::raw::c_char;
13use std::os::unix::io::AsRawFd;
14use std::ptr;
15use std::str::FromStr;
16use tracing::{debug, trace, warn};
17
18#[derive(Debug)]
19#[repr(i32)]
20pub enum IpmgmtCmd {
21    Unset = 0,
22    SetProp = 1,
23    SetIf = 2,
24    SetAddr = 3,
25    GetProp = 4,
26    GetIf = 5,
27    GetAddr = 6,
28    ResetIf = 7,
29    ResetAddr = 8,
30    ResetProp = 9,
31    InitIf = 10,
32    AddrobjLookupAdd = 11,
33    AddrobjSetLifnum = 12,
34    AddrobjAdd = 13,
35    Lif2Addrobj = 14,
36    AobjName2Addrobj = 15,
37}
38
39#[derive(Debug)]
40#[repr(C)]
41pub struct IpmgmtGetAddr {
42    pub cmd: IpmgmtCmd,
43    pub flags: u32,
44    pub ifname: [u8; 32],
45    pub family: u16,
46    pub objname: [u8; 64],
47}
48
49#[derive(Debug)]
50#[repr(C)]
51pub struct IpmgmtSetAddr {
52    pub cmd: IpmgmtCmd,
53    pub flags: u32,
54
55    //NOTE: this is a size_t in libipadm.h which is bad news for doors
56    pub nvlsize: u32,
57}
58
59impl Default for IpmgmtGetAddr {
60    fn default() -> Self {
61        IpmgmtGetAddr {
62            cmd: IpmgmtCmd::GetAddr,
63            flags: 0,
64            ifname: [0; 32],
65            family: 0,
66            objname: [0; 64],
67        }
68    }
69}
70
71#[derive(Debug, Default)]
72#[repr(C)]
73pub struct IpmgmtRval {
74    pub err: i32,
75}
76
77#[derive(Debug, Default)]
78#[repr(C)]
79pub struct IpmgmtGetRval {
80    pub err: i32,
81    pub nval_size: u32,
82    /* native-encoded nvlist follows*/
83}
84
85pub const LIFNAMSIZ: u32 = 32;
86pub const IPADM_AOBJ_USTRSIZ: u32 = 32;
87pub const IPADM_AOBJSIZ: u32 = LIFNAMSIZ + IPADM_AOBJ_USTRSIZ;
88
89#[derive(Debug, Default, Copy, Clone)]
90#[repr(i32)]
91pub enum AddrType {
92    #[default]
93    AddrNone,
94    Static,
95    Ipv6Addrconf,
96    Dhcp,
97}
98
99#[repr(C)]
100pub struct IpmgmtAobjopArg {
101    pub cmd: IpmgmtCmd,
102    pub flags: u32,
103    pub objname: [c_char; IPADM_AOBJSIZ as usize],
104    pub ifname: [c_char; LIFNAMSIZ as usize],
105    pub lnum: i32,
106    pub family: u16,
107    pub atype: AddrType,
108}
109
110#[repr(C)]
111#[derive(Copy, Clone)]
112pub struct IpmgmtAobjopRval {
113    pub err: i32,
114    pub objname: [c_char; IPADM_AOBJSIZ as usize],
115    pub ifname: [c_char; LIFNAMSIZ as usize],
116    pub lnum: i32,
117    pub family: u16,
118    pub flags: u32,
119    pub atype: AddrType,
120    pub atype_cache: IpmgmtAddrTypeCache,
121}
122
123impl Default for IpmgmtAobjopRval {
124    fn default() -> Self {
125        IpmgmtAobjopRval {
126            err: 0,
127            objname: [0; IPADM_AOBJSIZ as usize],
128            ifname: [0; LIFNAMSIZ as usize],
129            lnum: 0,
130            family: 0,
131            flags: 0,
132            atype: AddrType::default(),
133            atype_cache: IpmgmtAddrTypeCache::default(),
134        }
135    }
136}
137
138#[repr(C)]
139#[derive(Copy, Clone)]
140pub union IpmgmtAddrTypeCache {
141    pub ipv6_cache: IpmgmtIpv6Cache,
142    pub dhcp_cache: IpmgmtDhcpCache,
143}
144
145impl Default for IpmgmtAddrTypeCache {
146    fn default() -> Self {
147        IpmgmtAddrTypeCache {
148            ipv6_cache: IpmgmtIpv6Cache::default(),
149        }
150    }
151}
152
153// for C interop compat
154#[repr(C)]
155#[derive(Debug, Copy, Clone)]
156pub enum BooleanT {
157    False,
158    True,
159}
160
161#[repr(C)]
162#[derive(Copy, Clone)]
163pub struct IpmgmtIpv6Cache {
164    pub linklocal: BooleanT,
165    pub ifid: sockaddr_in6,
166}
167
168impl Default for IpmgmtIpv6Cache {
169    fn default() -> Self {
170        IpmgmtIpv6Cache {
171            linklocal: BooleanT::False,
172            ifid: sockaddr_in6 {
173                sin6_family: 0,
174                sin6_port: 0,
175                sin6_flowinfo: 0,
176                sin6_addr: libc::in6_addr { s6_addr: [0; 16] },
177                sin6_scope_id: 0,
178                ..unsafe { std::mem::zeroed() }
179            },
180        }
181    }
182}
183
184#[repr(C)]
185#[derive(Debug, Copy, Clone)]
186pub struct IpmgmtDhcpCache {
187    pub reqhost: [c_char; 256usize],
188}
189
190impl Default for IpmgmtDhcpCache {
191    fn default() -> Self {
192        IpmgmtDhcpCache { reqhost: [0; 256] }
193    }
194}
195
196#[derive(Debug)]
197#[repr(C)]
198pub struct IpmgmtAddrArg {
199    pub cmd: IpmgmtCmd,
200    pub flags: u32,
201    pub objname: [c_char; IPADM_AOBJSIZ as usize],
202    pub lnum: u32,
203}
204
205#[derive(Debug)]
206pub struct IpInfo {
207    pub if_name: String,
208    pub addr_obj: String,
209    pub properties: IpProperties,
210}
211
212#[derive(Debug)]
213pub enum IpProperties {
214    Dhcp(DhcpProperties),
215    Intfid(Intfid),
216    V4Static(V4Static),
217    V6Static(V6Static),
218}
219
220#[derive(Debug)]
221pub struct DhcpProperties {
222    pub parameters: Option<DhcpClientParameters>,
223    pub reqhost: Option<String>,
224}
225
226#[derive(Debug)]
227pub struct DhcpClientParameters {
228    pub wait: i32,
229    pub primary: bool,
230}
231
232#[derive(Debug)]
233pub struct V4Static {
234    pub addr: Ipv4Addr,
235}
236
237#[derive(Debug)]
238pub struct V6Static {
239    pub addr: Ipv6Addr,
240}
241
242#[derive(Debug)]
243pub struct Intfid {
244    pub prefix_len: u32,
245    pub addr: Ipv6Addr,
246    pub stateless: bool,
247    pub stateful: bool,
248}
249
250pub fn get_persistent_ipinfo(
251) -> Result<HashMap<String, HashMap<String, IpInfo>>, String> {
252    unsafe {
253        //// call the ipadmd door to get address information
254
255        let f = File::open("/etc/svc/volatile/ipadm/ipmgmt_door")
256            .map_err(|e| format!("door open: {}", e))?;
257
258        // This memory may get realloc'd by the door call, so we cannot use a
259        // Box :/
260        let mut response: *mut IpmgmtGetRval =
261            malloc(std::mem::size_of::<IpmgmtGetRval>()) as *mut IpmgmtGetRval;
262
263        let request = IpmgmtGetAddr {
264            ..Default::default()
265        };
266        let resp: *mut IpmgmtGetRval = door_callp(
267            f.as_raw_fd(),
268            request,
269            ptr::NonNull::new(&mut response).unwrap(), // null not possible
270        );
271        trace!("got {} bytes of nval", (*resp).nval_size);
272
273        //// extract nvlist  header
274
275        let nvh = (response.offset(1) as *const IpmgmtGetRval) as *mut NvHeader;
276        trace!("found nvl header {:?}", *nvh);
277
278        //// set up iteration pointer and consumption counter
279
280        // NOTE! somehwere in the packing process an 8 byte padd is added
281        // between the header and the first nvpair
282        let sk = ((nvh.offset(1) as *const NvHeader) as *const u8).offset(0);
283        let skipped = std::slice::from_raw_parts(sk, 8);
284        warn!("skipping {:x?}", skipped);
285        let p = ((nvh.offset(1) as *const NvHeader) as *const u8).offset(8);
286
287        // NOTE! i've observed that `nval_size` can be larger than the
288        // actual size of the list. We are relying on zero-sized nvpair
289        // detection to exit processing lists. However, if there is ever a list
290        // that is not terminated with a zero-sized nvpair AND the `nval_size` is
291        // larger than the list actually is, we can get undefined behavior. The
292        // `end` property is plumbed down to the `extract_nvps` function
293        // primarily as a safeguard to keep the iteration from running unbounded
294        // in the case that the nvlist is not terminated with a zero-sized nvpair.
295        let end = (*resp).nval_size as i32;
296
297        //// extract the name value pairs
298
299        let (nvps, _) = extract_nvps(p, end);
300        trace!("NVPs: {:#?}", nvps);
301
302        free(resp as *mut std::os::raw::c_void);
303
304        Ok(handle_nvps(&nvps))
305    }
306}
307
308fn handle_nvps(
309    nvps: &[NVP<'static>],
310) -> HashMap<String, HashMap<String, IpInfo>> {
311    let mut result = HashMap::new();
312
313    for nvp in nvps.iter() {
314        let ip_info = match handle_nvp(nvp) {
315            Some(ip_info) => ip_info,
316            None => continue,
317        };
318
319        match result.get_mut(&ip_info.if_name) {
320            None => {
321                let mut hm = HashMap::new();
322                let k = ip_info.if_name.clone();
323                hm.insert(ip_info.addr_obj.clone(), ip_info);
324                result.insert(k, hm);
325            }
326            Some(hm) => match hm.get_mut(&ip_info.addr_obj.clone()) {
327                Some(ipi) => {
328                    if let IpProperties::Dhcp(dcp) = &mut ipi.properties {
329                        if let IpProperties::Dhcp(dcpi) = ip_info.properties {
330                            if let Some(ps) = dcpi.parameters {
331                                dcp.parameters = Some(ps)
332                            }
333                            if let Some(rh) = dcpi.reqhost {
334                                dcp.reqhost = Some(rh)
335                            }
336                        }
337                    }
338                }
339                None => {
340                    hm.insert(ip_info.addr_obj.clone(), ip_info);
341                }
342            },
343        }
344    }
345
346    debug!("ipinfos: {:#?}", result);
347
348    result
349}
350
351// TODO this cornucopia of sadness shows why nvlists should be handled by serde
352// ....
353fn handle_nvp(nvp: &NVP<'static>) -> Option<IpInfo> {
354    let parts = match &nvp.value {
355        Value::NvList(parts) => parts,
356        _ => {
357            // having a property all on it's own makes no sense, ipmgmtd entries
358            // always come with at least an `_ifname` and some other property
359            warn!(
360                "Disaggregated ipmgmt property detected: {}. Skipping",
361                nvp.name
362            );
363            return None;
364        }
365    };
366
367    let mut if_name: Option<String> = None;
368    let mut addr_obj: Option<String> = None;
369    let mut ip_properties: Option<IpProperties> = None;
370
371    for part in parts.iter() {
372        if part.name == "_ifname" {
373            if let Value::Str(s) = part.value {
374                if_name = Some(s.to_string());
375            }
376        }
377
378        if part.name == "_aobjname" {
379            if let Value::Str(s) = part.value {
380                addr_obj = Some(s.to_string());
381            }
382        }
383
384        if part.name == "_dhcp" {
385            if let Value::NvList(vs) = &part.value {
386                let mut dcp = DhcpClientParameters {
387                    wait: -1,
388                    primary: false,
389                };
390                for v in vs.iter() {
391                    if v.name == "wait" {
392                        if let Value::Int32(i) = v.value {
393                            dcp.wait = i;
394                        }
395                    }
396                    if v.name == "primary" {
397                        if let Value::Boolean(b) = v.value {
398                            dcp.primary = b;
399                        }
400                    }
401                }
402                match &mut ip_properties {
403                    Some(props) => match props {
404                        IpProperties::Dhcp(props) => {
405                            props.parameters = Some(dcp)
406                        }
407                        _ => {
408                            warn!("dhcp client params in non-dhcp record");
409                        }
410                    },
411                    None => {
412                        ip_properties =
413                            Some(IpProperties::Dhcp(DhcpProperties {
414                                parameters: Some(dcp),
415                                reqhost: None,
416                            }));
417                    }
418                }
419            };
420        }
421
422        if part.name == "reqhost" {
423            if let Value::Str(s) = &part.value {
424                match &mut ip_properties {
425                    Some(props) => match props {
426                        IpProperties::Dhcp(props) => {
427                            props.reqhost = Some(s.to_string());
428                        }
429                        _ => {
430                            warn!("dhcp reqhost found in non-dhcp record")
431                        }
432                    },
433                    None => {
434                        ip_properties =
435                            Some(IpProperties::Dhcp(DhcpProperties {
436                                parameters: None,
437                                reqhost: Some(s.to_string()),
438                            }));
439                    }
440                }
441            }
442        }
443
444        if part.name == "_intfid" {
445            if let Value::NvList(vs) = &part.value {
446                let mut intfid = Intfid {
447                    prefix_len: 0,
448                    addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
449                    stateless: false,
450                    stateful: false,
451                };
452                for v in vs.iter() {
453                    if v.name == "prefixlen" {
454                        if let Value::Uint32(u) = v.value {
455                            intfid.prefix_len = u;
456                        }
457                    }
458                    if v.name == "_addr" {
459                        if let Value::Uint8Array(u) = v.value {
460                            intfid.addr = match u.try_into()
461                                as Result<
462                                    [u8; 16],
463                                    <&[u8] as TryInto<[u8; 16]>>::Error,
464                                > {
465                                Ok(addr) => Ipv6Addr::from(addr),
466                                Err(_) => continue,
467                            }
468                        }
469                    }
470                    if v.name == "_stateless" {
471                        if let Value::Str(b) = v.value {
472                            if b == "yes" {
473                                intfid.stateless = true;
474                            }
475                        }
476                    }
477                    if v.name == "_stateful" {
478                        if let Value::Str(b) = v.value {
479                            if b == "yes" {
480                                intfid.stateful = true
481                            }
482                        }
483                    }
484                }
485                match &mut ip_properties {
486                    Some(_) => {
487                        warn!("duplicate v6 properties");
488                    }
489                    None => {
490                        ip_properties = Some(IpProperties::Intfid(intfid));
491                    }
492                }
493            }
494        }
495
496        if part.name == "_ipv4addr" {
497            if let Value::NvList(vs) = &part.value {
498                for v in vs.iter() {
499                    if let Value::Str(s) = &v.value {
500                        match Ipv4Addr::from_str(s) {
501                            Ok(addr) => {
502                                ip_properties =
503                                    Some(IpProperties::V4Static(V4Static {
504                                        addr,
505                                    }));
506                            }
507                            _ => {
508                                warn!("bad ipv4 address: {}", s)
509                            }
510                        }
511                    }
512                }
513            }
514        }
515
516        if part.name == "_ipv6addr" {
517            if let Value::NvList(vs) = &part.value {
518                for v in vs.iter() {
519                    if let Value::Str(s) = &v.value {
520                        match Ipv6Addr::from_str(s) {
521                            Ok(addr) => {
522                                ip_properties =
523                                    Some(IpProperties::V6Static(V6Static {
524                                        addr,
525                                    }));
526                            }
527                            _ => {
528                                warn!("bad ipv6 address: {}", s)
529                            }
530                        }
531                    }
532                }
533            }
534        }
535    }
536
537    match (if_name, addr_obj, ip_properties) {
538        (Some(n), Some(a), Some(p)) => Some(IpInfo {
539            if_name: n,
540            addr_obj: a,
541            properties: p,
542        }),
543        _ => None,
544    }
545}
546
547fn extract_nvps(mut p: *const u8, size: i32) -> (Vec<NVP<'static>>, i32) {
548    let mut nvps = Vec::new();
549    let mut consumed = 0;
550
551    unsafe {
552        //// Iterate over nvpairs in the nvlist
553
554        loop {
555            let sz = *(p as *const i32);
556            if sz == 0 {
557                trace!("found zero sized nvpair, return from extract");
558                consumed += 4;
559                //p = p.add(4);
560                return (nvps, consumed);
561            }
562            let nvp = p as *const NvPair;
563            let nv = match extract_nvp(nvp) {
564                Ok(nv) => nv,
565                Err(e) => {
566                    warn!("nv extraction failed: {}", e);
567                    continue;
568                }
569            };
570
571            p = p.add(sz as usize);
572            p = p.add(p.align_offset(align_of::<u32>()));
573            consumed += sz;
574            trace!("consumed {}", consumed);
575            if consumed >= size {
576                break;
577            }
578
579            match nv.value {
580                Value::NvList(_) => {
581                    let (embedded_nvps, embedded_consumed) =
582                        extract_nvps(p, size - consumed);
583
584                    nvps.push(NVP {
585                        name: nv.name,
586                        value: Value::NvList(embedded_nvps),
587                    });
588                    consumed += embedded_consumed;
589                    p = p.add(embedded_consumed as usize);
590                }
591                _ => {
592                    nvps.push(nv);
593                }
594            }
595        }
596    }
597
598    (nvps, consumed)
599}
600
601fn extract_nvp(nvp: *const NvPair) -> Result<NVP<'static>, String> {
602    let mut result = NVP {
603        name: "?",
604        value: Value::Unknown,
605    };
606
607    unsafe {
608        //// extract name
609
610        trace!("nvp: {:?}", *nvp);
611        let p = (nvp.offset(1) as *const u8) as *mut u8;
612        let name = {
613            let slice =
614                std::slice::from_raw_parts(p, (*nvp).name_size as usize);
615            let cstr = std::ffi::CStr::from_bytes_with_nul_unchecked(slice);
616            match cstr.to_str() {
617                Ok(s) => s,
618                Err(e) => return Err(format!("name to string: {}", e)),
619            }
620        };
621        result.name = name;
622        trace!("  name: {:?}", name);
623
624        //// extract value
625
626        let mut v = p.offset((*nvp).name_size as isize);
627        v = v.add(v.align_offset(align_of::<u32>()));
628
629        match (*nvp).typ {
630            NvDataType::Str => {
631                let slice = std::slice::from_raw_parts(
632                    v,
633                    ((*nvp).size
634                        - (*nvp).name_size as i32
635                        - size_of::<NvPair>() as i32
636                        - 1) as usize,
637                );
638                let cstr = std::ffi::CStr::from_bytes_with_nul_unchecked(slice);
639                let decoded = cstr.to_str().unwrap_or("<undecodable value>");
640                //TODO no idea why some strings come back with leading and
641                //trailing zeros
642                let trimmed = decoded.trim_matches('\u{0}');
643                result.value = Value::Str(trimmed);
644                trace!("  value: {:?}", trimmed);
645            }
646
647            NvDataType::BooleanValue => {
648                let b = *(v as *const bool);
649                result.value = Value::Boolean(b);
650                trace!("  value: {}", b);
651            }
652
653            NvDataType::Int32 => {
654                let i = *(v as *const i32);
655                result.value = Value::Int32(i);
656                trace!("  value: {}", i);
657            }
658
659            NvDataType::Uint32 => {
660                let u = *(v as *const u32);
661                result.value = Value::Uint32(u);
662                trace!("  value: {}", u);
663            }
664
665            NvDataType::Uint8Array => {
666                let ua =
667                    std::slice::from_raw_parts(v, (*nvp).value_count as usize);
668                result.value = Value::Uint8Array(ua);
669                trace!("  value: {:x?}", ua);
670            }
671
672            NvDataType::NvList => {
673                result.value = Value::NvList(Vec::new());
674            }
675
676            _ => {}
677        }
678    }
679
680    Ok(result)
681}
682
683//TODO this interface should return all information contained in addrobj_rtal_t
684pub fn ifname_to_addrobj(
685    mut if_name: &str,
686    addr_family: u16,
687) -> Result<(String, String), String> {
688    let parts: Vec<&str> = if_name.split(':').collect();
689    let num = match parts.len() {
690        2 => match parts[1].parse::<i32>() {
691            Ok(n) => {
692                if_name = parts[0];
693                n
694            }
695            Err(_) => 0,
696        },
697        _ => 0,
698    };
699
700    let mut ia_ifname = [0; 32usize];
701    for (i, c) in if_name.chars().enumerate() {
702        ia_ifname[i] = c as std::os::raw::c_char;
703    }
704
705    let request = crate::sys::ipmgmt_aobjop_arg_t {
706        ia_ifname,
707        ia_cmd: crate::sys::ipmgmt_door_cmd_type_t_IPMGMT_CMD_LIF2ADDROBJ,
708        ia_flags: 0,
709        ia_aobjname: [0; 64usize],
710        ia_lnum: num,
711        ia_family: addr_family,
712        ia_atype: crate::sys::ipadm_addr_type_t_IPADM_ADDR_NONE,
713    };
714
715    let f = File::open("/etc/svc/volatile/ipadm/ipmgmt_door")
716        .map_err(|e| format!("door open: {}", e))?;
717
718    let resp: crate::sys::ipmgmt_aobjop_rval_t =
719        door_call(f.as_raw_fd(), request);
720
721    let objname = unsafe {
722        CStr::from_ptr(resp.ir_aobjname.as_ptr())
723            .to_str()
724            .map_err(|e| format!("abojname cstr to str: {}", e))?
725            .to_string()
726    };
727
728    let source = match resp.ir_atype {
729        crate::sys::ipadm_addr_type_t_IPADM_ADDR_NONE => "none",
730        crate::sys::ipadm_addr_type_t_IPADM_ADDR_STATIC => "static",
731        crate::sys::ipadm_addr_type_t_IPADM_ADDR_IPV6_ADDRCONF => "addrconf",
732        crate::sys::ipadm_addr_type_t_IPADM_ADDR_DHCP => "dhcp",
733        _ => "?",
734    };
735
736    Ok((objname, source.to_string()))
737}
738
739//TODO this interface should return all information contained in addrobj_rtal_t
740//      in a sane way e.g. not a tuple
741pub fn addrobjname_to_addrobj(
742    aobj_name: &str,
743) -> Result<(String, String, u16, String, i32), String> {
744    let mut request = crate::sys::ipmgmt_aobjop_arg_t {
745        ia_cmd: crate::sys::ipmgmt_door_cmd_type_t_IPMGMT_CMD_AOBJNAME2ADDROBJ,
746        ia_flags: 0,
747        ia_aobjname: [0; 64usize],
748        ia_ifname: [0; 32usize],
749        ia_lnum: 0,
750        ia_family: 0,
751        ia_atype: crate::sys::ipadm_addr_type_t_IPADM_ADDR_NONE,
752    };
753    for (i, c) in aobj_name.chars().enumerate() {
754        request.ia_aobjname[i] = c as std::os::raw::c_char;
755    }
756
757    let f = File::open("/etc/svc/volatile/ipadm/ipmgmt_door")
758        .map_err(|e| format!("door open: {}", e))?;
759
760    let resp: crate::sys::ipmgmt_aobjop_rval_t =
761        door_call(f.as_raw_fd(), request);
762
763    let ifname = unsafe {
764        CStr::from_ptr(resp.ir_ifname.as_ptr())
765            .to_str()
766            .map_err(|e| format!("abojname cstr to str: {}", e))?
767            .to_string()
768    };
769
770    let source = match resp.ir_atype {
771        crate::sys::ipadm_addr_type_t_IPADM_ADDR_NONE => "none",
772        crate::sys::ipadm_addr_type_t_IPADM_ADDR_STATIC => "static",
773        crate::sys::ipadm_addr_type_t_IPADM_ADDR_IPV6_ADDRCONF => "addrconf",
774        crate::sys::ipadm_addr_type_t_IPADM_ADDR_DHCP => "dhcp",
775        _ => "?",
776    };
777
778    Ok((
779        aobj_name.to_string(),
780        source.to_string(),
781        resp.ir_family,
782        ifname,
783        resp.ir_lnum,
784    ))
785}