Skip to main content

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