dhcpv4/
configuration.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#[cfg(target_os = "fuchsia")]
6use crate::protocol::{FidlCompatible, FromFidlExt, IntoFidlExt};
7
8#[cfg(target_os = "fuchsia")]
9use anyhow::Context;
10
11#[cfg(target_os = "fuchsia")]
12use std::convert::Infallible as Never;
13
14use net_types::ip::{IpAddress as _, Ipv4, PrefixLength};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::io;
18use std::net::Ipv4Addr;
19use std::num::TryFromIntError;
20use thiserror::Error;
21
22/// A collection of the basic configuration parameters needed by the server.
23#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
24pub struct ServerParameters {
25    /// The IPv4 addresses of the host running the server.
26    pub server_ips: Vec<Ipv4Addr>,
27    /// The duration for which leases should be assigned to clients
28    pub lease_length: LeaseLength,
29    /// The IPv4 addresses which the server is responsible for managing and leasing to
30    /// clients.
31    pub managed_addrs: ManagedAddresses,
32    /// A list of MAC addresses which are permitted to request a lease. If empty, any MAC address
33    /// may request a lease.
34    pub permitted_macs: PermittedMacs,
35    /// A collection of static address assignments. Any client whose MAC address has a static
36    /// assignment will be offered the assigned IP address.
37    pub static_assignments: StaticAssignments,
38    /// Enables server behavior where the server ARPs an IP address prior to issuing
39    /// it in a lease.
40    pub arp_probe: bool,
41    /// The interface names to which the server's UDP sockets are bound. If
42    /// this vector is empty, the server will not bind to a specific interface
43    /// and will process incoming DHCP messages regardless of the interface on
44    /// which they arrive.
45    pub bound_device_names: Vec<String>,
46}
47
48impl ServerParameters {
49    pub fn is_valid(&self) -> bool {
50        let Self {
51            server_ips,
52            lease_length: crate::configuration::LeaseLength { default_seconds, max_seconds },
53            managed_addrs:
54                crate::configuration::ManagedAddresses { mask: _, pool_range_start, pool_range_stop },
55            permitted_macs: _,
56            static_assignments: _,
57            arp_probe: _,
58            bound_device_names: _,
59        } = self;
60        if server_ips.is_empty() {
61            return false;
62        }
63        if [pool_range_start, pool_range_stop]
64            .into_iter()
65            .chain(server_ips.iter())
66            .any(std::net::Ipv4Addr::is_unspecified)
67        {
68            return false;
69        }
70        if *default_seconds == 0 {
71            return false;
72        }
73        if *max_seconds == 0 {
74            return false;
75        }
76        true
77    }
78}
79
80/// Parameters controlling lease duration allocation. Per,
81/// https://tools.ietf.org/html/rfc2131#section-3.3, times are represented as relative times.
82#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
83pub struct LeaseLength {
84    /// The default lease duration assigned by the server.
85    pub default_seconds: u32,
86    /// The maximum allowable lease duration which a client can request.
87    pub max_seconds: u32,
88}
89
90#[cfg(target_os = "fuchsia")]
91impl FidlCompatible<fidl_fuchsia_net_dhcp::LeaseLength> for LeaseLength {
92    type FromError = anyhow::Error;
93    type IntoError = Never;
94
95    fn try_from_fidl(fidl: fidl_fuchsia_net_dhcp::LeaseLength) -> Result<Self, Self::FromError> {
96        if let fidl_fuchsia_net_dhcp::LeaseLength { default: Some(default_seconds), max, .. } = fidl
97        {
98            Ok(LeaseLength {
99                default_seconds,
100                // Per fuchsia.net.dhcp, if omitted, max defaults to the value of default.
101                max_seconds: max.unwrap_or(default_seconds),
102            })
103        } else {
104            Err(anyhow::format_err!(
105                "fuchsia.net.dhcp.LeaseLength missing required field: {:?}",
106                fidl
107            ))
108        }
109    }
110
111    fn try_into_fidl(self) -> Result<fidl_fuchsia_net_dhcp::LeaseLength, Self::IntoError> {
112        let LeaseLength { default_seconds, max_seconds } = self;
113        Ok(fidl_fuchsia_net_dhcp::LeaseLength {
114            default: Some(default_seconds),
115            max: Some(max_seconds),
116            ..Default::default()
117        })
118    }
119}
120
121/// The IP addresses which the server will manage and lease to clients.
122#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
123pub struct ManagedAddresses {
124    /// The subnet mask of the subnet for which the server will manage addresses.
125    pub mask: SubnetMask,
126    /// The inclusive starting address of the range of managed addresses.
127    pub pool_range_start: Ipv4Addr,
128    /// The exclusive stopping address of the range of managed addresses.
129    pub pool_range_stop: Ipv4Addr,
130}
131
132impl ManagedAddresses {
133    fn pool_range_inner(&self) -> std::ops::Range<u32> {
134        let Self { mask: _, pool_range_start, pool_range_stop } = *self;
135        pool_range_start.into()..pool_range_stop.into()
136    }
137    /// Returns an iterator of the `Ipv4Addr`s from `pool_range_start`, inclusive, to
138    /// `pool_range_stop`, exclusive.
139    pub fn pool_range(&self) -> impl Iterator<Item = Ipv4Addr> {
140        self.pool_range_inner().map(Into::into)
141    }
142
143    /// Returns the number of `Ipv4Addr`s from `pool_range_start`, inclusive, to
144    /// `pool_range_stop`, exclusive.
145    pub fn pool_range_size(&self) -> Result<u32, TryFromIntError> {
146        self.pool_range_inner().len().try_into()
147    }
148}
149
150#[cfg(target_os = "fuchsia")]
151impl FidlCompatible<fidl_fuchsia_net_dhcp::AddressPool> for ManagedAddresses {
152    type FromError = anyhow::Error;
153    type IntoError = Never;
154
155    fn try_from_fidl(fidl: fidl_fuchsia_net_dhcp::AddressPool) -> Result<Self, Self::FromError> {
156        if let fidl_fuchsia_net_dhcp::AddressPool {
157            prefix_length: Some(prefix_length),
158            range_start: Some(pool_range_start),
159            range_stop: Some(pool_range_stop),
160            ..
161        } = fidl
162        {
163            let mask = PrefixLength::new(prefix_length).map(SubnetMask::new).map_err(
164                |net_types::ip::PrefixTooLongError| {
165                    anyhow::format_err!(
166                        "failed to create subnet mask from prefix_length={}",
167                        prefix_length
168                    )
169                },
170            )?;
171            let pool_range_start = Ipv4Addr::from_fidl(pool_range_start);
172            let pool_range_stop = Ipv4Addr::from_fidl(pool_range_stop);
173            let addresses_candidate = Self { mask, pool_range_start, pool_range_stop };
174            if pool_range_start > pool_range_stop {
175                return Err(anyhow::format_err!(
176                    "fuchsia.net.dhcp.AddressPool contained range_start ({}) > range_stop ({})",
177                    pool_range_start,
178                    pool_range_stop
179                ));
180            }
181            let pool_range_size = addresses_candidate.pool_range_size().with_context(|| {
182                format!("failed to determine address pool size for range_start ({}) and range_stop ({})", pool_range_start, pool_range_stop)
183            })?;
184            if pool_range_size > mask.subnet_size() {
185                Err(anyhow::format_err!(
186                    "fuchsia.net.dhcp.AddressPool contained prefix_length ({}) which cannot fit address pool defined by range_start: ({}) and range_stop: ({})",
187                    prefix_length,
188                    pool_range_start,
189                    pool_range_stop
190                ))
191            } else {
192                Ok(addresses_candidate)
193            }
194        } else {
195            Err(anyhow::format_err!("fuchsia.net.dhcp.AddressPool missing fields: {:?}", fidl))
196        }
197    }
198
199    fn try_into_fidl(self) -> Result<fidl_fuchsia_net_dhcp::AddressPool, Self::IntoError> {
200        let ManagedAddresses { mask, pool_range_start, pool_range_stop } = self;
201        Ok(fidl_fuchsia_net_dhcp::AddressPool {
202            prefix_length: Some(mask.ones()),
203            range_start: Some(pool_range_start.into_fidl()),
204            range_stop: Some(pool_range_stop.into_fidl()),
205            ..Default::default()
206        })
207    }
208}
209
210/// A list of MAC addresses which are permitted to request a lease.
211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
212pub struct PermittedMacs(pub Vec<fidl_fuchsia_net_ext::MacAddress>);
213
214#[cfg(target_os = "fuchsia")]
215impl FidlCompatible<Vec<fidl_fuchsia_net::MacAddress>> for PermittedMacs {
216    type FromError = Never;
217    type IntoError = Never;
218
219    fn try_from_fidl(fidl: Vec<fidl_fuchsia_net::MacAddress>) -> Result<Self, Self::FromError> {
220        Ok(PermittedMacs(fidl.into_iter().map(|mac| mac.into()).collect()))
221    }
222
223    fn try_into_fidl(self) -> Result<Vec<fidl_fuchsia_net::MacAddress>, Self::IntoError> {
224        Ok(self.0.into_iter().map(|mac| mac.into()).collect())
225    }
226}
227
228/// A collection of static address assignments. Any client whose MAC address has a static
229/// assignment will be offered the assigned IP address.
230#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
231pub struct StaticAssignments(pub HashMap<fidl_fuchsia_net_ext::MacAddress, Ipv4Addr>);
232
233#[cfg(target_os = "fuchsia")]
234impl FidlCompatible<Vec<fidl_fuchsia_net_dhcp::StaticAssignment>> for StaticAssignments {
235    type FromError = anyhow::Error;
236    type IntoError = Never;
237
238    fn try_from_fidl(
239        fidl: Vec<fidl_fuchsia_net_dhcp::StaticAssignment>,
240    ) -> Result<Self, Self::FromError> {
241        match fidl.into_iter().try_fold(HashMap::new(), |mut acc, assignment| {
242            if let (Some(host), Some(assigned_addr)) = (assignment.host, assignment.assigned_addr) {
243                let mac = fidl_fuchsia_net_ext::MacAddress::from(host);
244                match acc.insert(mac, Ipv4Addr::from_fidl(assigned_addr)) {
245                    Some(_ip) => Err(anyhow::format_err!(
246                        "fuchsia.net.dhcp.StaticAssignment contained multiple entries for {}",
247                        mac
248                    )),
249                    None => Ok(acc),
250                }
251            } else {
252                Err(anyhow::format_err!(
253                    "fuchsia.net.dhcp.StaticAssignment contained entry with missing fields: {:?}",
254                    assignment
255                ))
256            }
257        }) {
258            Ok(static_assignments) => Ok(StaticAssignments(static_assignments)),
259            Err(e) => Err(e),
260        }
261    }
262
263    fn try_into_fidl(
264        self,
265    ) -> Result<Vec<fidl_fuchsia_net_dhcp::StaticAssignment>, Self::IntoError> {
266        Ok(self
267            .0
268            .into_iter()
269            .map(|(host, assigned_addr)| fidl_fuchsia_net_dhcp::StaticAssignment {
270                host: Some(host.into()),
271                assigned_addr: Some(assigned_addr.into_fidl()),
272                ..Default::default()
273            })
274            .collect())
275    }
276}
277
278/// A wrapper around the error types which can be returned when loading a
279/// `ServerConfig` from file with `load_server_config_from_file()`.
280#[derive(Debug, Error)]
281pub enum ConfigError {
282    #[error("io error: {}", _0)]
283    IoError(io::Error),
284    #[error("json deserialization error: {}", _0)]
285    JsonError(serde_json::Error),
286}
287
288impl From<io::Error> for ConfigError {
289    fn from(e: io::Error) -> Self {
290        ConfigError::IoError(e)
291    }
292}
293
294impl From<serde_json::Error> for ConfigError {
295    fn from(e: serde_json::Error) -> Self {
296        ConfigError::JsonError(e)
297    }
298}
299
300/// A bitmask which represents the boundary between the Network part and Host part of an IPv4
301/// address.
302#[derive(Clone, Copy, Debug, PartialEq)]
303pub struct SubnetMask {
304    // The PrefixLength representing the subnet mask.
305    prefix_length: PrefixLength<Ipv4>,
306}
307
308mod serde_impls {
309    use net_types::ip::PrefixLength;
310    use serde::de::Error as _;
311    use serde::{Deserialize, Serialize};
312
313    // In order to preserve compatibility with a previous representation of
314    // `SubnetMask`, we implement Serialize and Deserialize by forwarding those
315    // methods to derived impls on the old representation.
316    #[derive(Serialize, Deserialize)]
317    struct SubnetMask {
318        ones: u8,
319    }
320
321    impl Serialize for super::SubnetMask {
322        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
323        where
324            S: serde::Serializer,
325        {
326            let Self { prefix_length } = self;
327            SubnetMask { ones: prefix_length.get() }.serialize(serializer)
328        }
329    }
330
331    impl<'de> Deserialize<'de> for super::SubnetMask {
332        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
333        where
334            D: serde::Deserializer<'de>,
335        {
336            let SubnetMask { ones } = Deserialize::deserialize(deserializer)?;
337            Ok(super::SubnetMask {
338                prefix_length: PrefixLength::new(ones).map_err(
339                    |net_types::ip::PrefixTooLongError| {
340                        D::Error::custom(format!("{ones} too long to be IPv4 prefix length"))
341                    },
342                )?,
343            })
344        }
345    }
346}
347
348impl SubnetMask {
349    /// Constructs a new `SubnetMask`.
350    pub const fn new(prefix_length: PrefixLength<Ipv4>) -> Self {
351        SubnetMask { prefix_length }
352    }
353
354    /// Returns a byte-array representation of the `SubnetMask` in Network (Big-Endian) byte-order.
355    pub fn octets(&self) -> [u8; 4] {
356        let Self { prefix_length } = self;
357        prefix_length.get_mask().ipv4_bytes()
358    }
359
360    fn to_u32(&self) -> u32 {
361        u32::from_be_bytes(self.octets())
362    }
363
364    /// Returns the count of the set high-order bits of the `SubnetMask`.
365    pub fn ones(&self) -> u8 {
366        let Self { prefix_length } = self;
367        prefix_length.get()
368    }
369
370    /// Returns the network address resulting from masking the argument.
371    pub fn apply_to(&self, target: &Ipv4Addr) -> Ipv4Addr {
372        let Self { prefix_length } = self;
373        net_types::ip::Ipv4Addr::from(*target).mask(prefix_length.get()).into()
374    }
375
376    /// Computes the broadcast address for the argument.
377    pub fn broadcast_of(&self, target: &Ipv4Addr) -> Ipv4Addr {
378        let subnet_mask_bits = self.to_u32();
379        let target_bits = u32::from_be_bytes(target.octets());
380        Ipv4Addr::from(!subnet_mask_bits | target_bits)
381    }
382
383    /// Returns the size of the subnet defined by this mask.
384    pub fn subnet_size(&self) -> u32 {
385        !self.to_u32()
386    }
387}
388
389impl TryFrom<Ipv4Addr> for SubnetMask {
390    type Error = anyhow::Error;
391
392    fn try_from(mask: Ipv4Addr) -> Result<Self, Self::Error> {
393        Ok(SubnetMask {
394            prefix_length: PrefixLength::try_from_subnet_mask(net_types::ip::Ipv4Addr::from(mask))
395                .map_err(|net_types::ip::NotSubnetMaskError| {
396                    anyhow::anyhow!("{mask} is not a valid subnet mask")
397                })?,
398        })
399    }
400}
401
402#[cfg(target_os = "fuchsia")]
403impl FidlCompatible<fidl_fuchsia_net::Ipv4Address> for SubnetMask {
404    type FromError = anyhow::Error;
405    type IntoError = Never;
406
407    fn try_from_fidl(fidl: fidl_fuchsia_net::Ipv4Address) -> Result<Self, Self::FromError> {
408        let addr = Ipv4Addr::from_fidl(fidl);
409        SubnetMask::try_from(addr)
410    }
411
412    fn try_into_fidl(self) -> Result<fidl_fuchsia_net::Ipv4Address, Self::IntoError> {
413        let addr = Ipv4Addr::from(self.to_u32());
414        Ok(addr.into_fidl())
415    }
416}
417
418impl From<SubnetMask> for Ipv4Addr {
419    fn from(value: SubnetMask) -> Self {
420        Self::from(value.to_u32())
421    }
422}
423
424impl From<SubnetMask> for PrefixLength<Ipv4> {
425    fn from(value: SubnetMask) -> Self {
426        let SubnetMask { prefix_length } = value;
427        prefix_length
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use crate::server::tests::{random_ipv4_generator, random_mac_generator};
435    use net_declare::{fidl_ip_v4, net_prefix_length_v4, std_ip_v4};
436
437    /// Asserts that the supplied Result is an err whose error string contains `substr`.
438    ///
439    /// We expect that the contained error implements Display, so that we can extract
440    /// that error string.
441    #[macro_export]
442    macro_rules! assert_err_with_substring {
443        ($result:expr, $substr:expr) => {{
444            match $result {
445                Err(e) => {
446                    let err_str = e.to_string();
447                    assert!(err_str.contains($substr), "{} not in {}", $substr, err_str)
448                }
449                Ok(v) => panic!(
450                    "{} (Ok({:?})) is not an Err containing {} ({})",
451                    stringify!($result),
452                    v,
453                    stringify!($substr),
454                    $substr
455                ),
456            }
457        }};
458    }
459
460    #[test]
461    fn try_from_ipv4addr_with_consecutive_ones_returns_mask() {
462        assert_eq!(
463            SubnetMask::try_from(std_ip_v4!("255.255.255.0"))
464                .expect("failed to create /24 subnet mask"),
465            SubnetMask { prefix_length: net_prefix_length_v4!(24) }
466        );
467        assert_eq!(
468            SubnetMask::try_from(std_ip_v4!("255.255.255.255"))
469                .expect("failed to create /32 subnet mask"),
470            SubnetMask { prefix_length: net_prefix_length_v4!(32) }
471        );
472    }
473
474    #[test]
475    fn try_from_ipv4addr_with_nonconsecutive_ones_returns_err() {
476        assert!(SubnetMask::try_from(std_ip_v4!("255.255.255.1")).is_err());
477    }
478
479    #[test]
480    fn lease_length_try_from_fidl() {
481        let both = fidl_fuchsia_net_dhcp::LeaseLength {
482            default: Some(42),
483            max: Some(42),
484            ..Default::default()
485        };
486        let with_default = fidl_fuchsia_net_dhcp::LeaseLength {
487            default: Some(42),
488            max: None,
489            ..Default::default()
490        };
491        let with_max = fidl_fuchsia_net_dhcp::LeaseLength {
492            default: None,
493            max: Some(42),
494            ..Default::default()
495        };
496        let neither =
497            fidl_fuchsia_net_dhcp::LeaseLength { default: None, max: None, ..Default::default() };
498
499        assert_eq!(
500            LeaseLength::try_from_fidl(both).unwrap(),
501            LeaseLength { default_seconds: 42, max_seconds: 42 }
502        );
503        assert_eq!(
504            LeaseLength::try_from_fidl(with_default).unwrap(),
505            LeaseLength { default_seconds: 42, max_seconds: 42 }
506        );
507        assert!(LeaseLength::try_from_fidl(with_max).is_err());
508        assert!(LeaseLength::try_from_fidl(neither).is_err());
509    }
510
511    #[test]
512    fn managed_addresses_try_from_fidl() {
513        let prefix_length = 24;
514        let start_addr = fidl_ip_v4!("192.168.0.2");
515        let stop_addr = fidl_ip_v4!("192.168.0.254");
516        let correct_pool = fidl_fuchsia_net_dhcp::AddressPool {
517            prefix_length: Some(prefix_length),
518            range_start: Some(start_addr),
519            range_stop: Some(stop_addr),
520            ..Default::default()
521        };
522
523        assert_matches::assert_matches!(
524            ManagedAddresses::try_from_fidl(correct_pool),
525            Ok(ManagedAddresses {
526                mask,
527                pool_range_start,
528                pool_range_stop,
529            }) if mask.ones() == prefix_length && pool_range_start.into_fidl() == start_addr && pool_range_stop.into_fidl() == stop_addr
530        );
531
532        let bad_prefix_length_pool = fidl_fuchsia_net_dhcp::AddressPool {
533            prefix_length: Some(33),
534            range_start: Some(fidl_ip_v4!("192.168.0.2")),
535            range_stop: Some(fidl_ip_v4!("192.168.0.254")),
536            ..Default::default()
537        };
538
539        assert_err_with_substring!(
540            ManagedAddresses::try_from_fidl(bad_prefix_length_pool),
541            "from prefix_length"
542        );
543
544        let missing_fields_pool = fidl_fuchsia_net_dhcp::AddressPool {
545            prefix_length: None,
546            range_start: Some(fidl_ip_v4!("192.168.0.2")),
547            range_stop: Some(fidl_ip_v4!("192.168.0.254")),
548            ..Default::default()
549        };
550
551        assert_err_with_substring!(
552            ManagedAddresses::try_from_fidl(missing_fields_pool),
553            "missing fields"
554        );
555
556        let start_after_stop_pool = fidl_fuchsia_net_dhcp::AddressPool {
557            prefix_length: Some(24),
558            range_start: Some(fidl_ip_v4!("192.168.0.20")),
559            range_stop: Some(fidl_ip_v4!("192.168.0.10")),
560            ..Default::default()
561        };
562
563        assert_err_with_substring!(
564            ManagedAddresses::try_from_fidl(start_after_stop_pool),
565            "> range_stop"
566        );
567
568        let mask_range_too_small_pool = fidl_fuchsia_net_dhcp::AddressPool {
569            prefix_length: Some(24),
570            range_start: Some(fidl_ip_v4!("192.168.0.0")),
571            range_stop: Some(fidl_ip_v4!("192.168.1.0")),
572            ..Default::default()
573        };
574
575        assert_err_with_substring!(
576            ManagedAddresses::try_from_fidl(mask_range_too_small_pool),
577            "cannot fit address pool"
578        );
579    }
580
581    #[test]
582    fn static_assignments_try_from_fidl() {
583        use std::iter::FromIterator;
584
585        let mac = random_mac_generator().bytes();
586        let ip = random_ipv4_generator();
587        let fields_present = vec![fidl_fuchsia_net_dhcp::StaticAssignment {
588            host: Some(fidl_fuchsia_net::MacAddress { octets: mac.clone() }),
589            assigned_addr: Some(ip.into_fidl()),
590            ..Default::default()
591        }];
592        let multiple_entries = vec![
593            fidl_fuchsia_net_dhcp::StaticAssignment {
594                host: Some(fidl_fuchsia_net::MacAddress { octets: mac.clone() }),
595                assigned_addr: Some(ip.into_fidl()),
596                ..Default::default()
597            },
598            fidl_fuchsia_net_dhcp::StaticAssignment {
599                host: Some(fidl_fuchsia_net::MacAddress { octets: mac.clone() }),
600                assigned_addr: Some(random_ipv4_generator().into_fidl()),
601                ..Default::default()
602            },
603        ];
604        let fields_missing = vec![fidl_fuchsia_net_dhcp::StaticAssignment {
605            host: None,
606            assigned_addr: None,
607            ..Default::default()
608        }];
609
610        assert_eq!(
611            StaticAssignments::try_from_fidl(fields_present).unwrap(),
612            StaticAssignments(HashMap::from_iter(
613                vec![(fidl_fuchsia_net_ext::MacAddress { octets: mac }, ip)].into_iter()
614            ))
615        );
616        assert!(StaticAssignments::try_from_fidl(multiple_entries).is_err());
617        assert!(StaticAssignments::try_from_fidl(fields_missing).is_err());
618    }
619}