wireguard_control/
device.rs

1use libc::c_char;
2
3use crate::{backends, key::Key, Backend, KeyPair, PeerConfigBuilder};
4
5use std::{
6    borrow::Cow,
7    ffi::CStr,
8    fmt, io,
9    net::{IpAddr, SocketAddr},
10    str::FromStr,
11    time::SystemTime,
12};
13
14/// Represents an IP address a peer is allowed to have, in CIDR notation.
15#[derive(PartialEq, Eq, Clone)]
16pub struct AllowedIp {
17    /// The IP address.
18    pub address: IpAddr,
19    /// The CIDR subnet mask.
20    pub cidr: u8,
21}
22
23impl fmt::Debug for AllowedIp {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "{}/{}", self.address, self.cidr)
26    }
27}
28
29impl std::str::FromStr for AllowedIp {
30    type Err = ();
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        let parts: Vec<_> = s.split('/').collect();
34        if parts.len() != 2 {
35            return Err(());
36        }
37
38        Ok(AllowedIp {
39            address: parts[0].parse().map_err(|_| ())?,
40            cidr: parts[1].parse().map_err(|_| ())?,
41        })
42    }
43}
44
45/// Represents a single peer's configuration (i.e. persistent attributes).
46///
47/// These are the attributes that don't change over time and are part of the configuration.
48#[derive(Debug, PartialEq, Eq, Clone)]
49#[non_exhaustive]
50pub struct PeerConfig {
51    /// The public key of the peer.
52    pub public_key: Key,
53    /// The preshared key available to both peers (`None` means no PSK is used).
54    pub preshared_key: Option<Key>,
55    /// The endpoint this peer listens for connections on (`None` means any).
56    pub endpoint: Option<SocketAddr>,
57    /// The interval for sending keepalive packets (`None` means disabled).
58    pub persistent_keepalive_interval: Option<u16>,
59    /// The IP addresses this peer is allowed to have.
60    pub allowed_ips: Vec<AllowedIp>,
61}
62
63/// Represents a single peer's current statistics (i.e. the data from the current session).
64///
65/// These are the attributes that will change over time; to update them,
66/// re-read the information from the interface.
67#[derive(Debug, PartialEq, Eq, Clone, Default)]
68pub struct PeerStats {
69    /// Time of the last handshake/rekey with this peer.
70    pub last_handshake_time: Option<SystemTime>,
71    /// Number of bytes received from this peer.
72    pub rx_bytes: u64,
73    /// Number of bytes transmitted to this peer.
74    pub tx_bytes: u64,
75}
76
77/// Represents the complete status of a peer.
78///
79/// This struct simply combines [`PeerInfo`](PeerInfo) and [`PeerStats`](PeerStats)
80/// to represent all available information about a peer.
81#[derive(Debug, PartialEq, Eq, Clone)]
82pub struct PeerInfo {
83    pub config: PeerConfig,
84    pub stats: PeerStats,
85}
86
87impl PeerInfo {
88    pub fn from_public_key(public_key: Key) -> PeerInfo {
89        PeerInfo {
90            config: PeerConfig {
91                public_key,
92                preshared_key: None,
93                endpoint: None,
94                persistent_keepalive_interval: None,
95                allowed_ips: vec![],
96            },
97            stats: PeerStats {
98                last_handshake_time: None,
99                rx_bytes: 0,
100                tx_bytes: 0,
101            },
102        }
103    }
104}
105
106/// Represents all available information about a WireGuard device (interface).
107///
108/// This struct contains the current configuration of the device
109/// and the current configuration _and_ state of all of its peers.
110/// The peer statistics are retrieved once at construction time,
111/// and need to be updated manually by calling [`get_by_name`](DeviceInfo::get_by_name).
112#[derive(Debug, PartialEq, Eq, Clone)]
113#[non_exhaustive]
114pub struct Device {
115    /// The interface name of this device
116    pub name: InterfaceName,
117    /// The public encryption key of this interface (if present)
118    pub public_key: Option<Key>,
119    /// The private encryption key of this interface (if present)
120    pub private_key: Option<Key>,
121    /// The [fwmark](https://www.linux.org/docs/man8/tc-fw.html) of this interface
122    pub fwmark: Option<u32>,
123    /// The port to listen for incoming connections on
124    pub listen_port: Option<u16>,
125    /// The list of all registered peers and their information
126    pub peers: Vec<PeerInfo>,
127    /// The associated "real name" of the interface (ex. "utun8" on macOS).
128    pub linked_name: Option<String>,
129    /// The backend the device exists on (userspace or kernel).
130    pub backend: Backend,
131}
132
133type RawInterfaceName = [c_char; libc::IFNAMSIZ];
134
135/// The name of a Wireguard interface device.
136#[derive(PartialEq, Eq, Clone, Copy)]
137pub struct InterfaceName(RawInterfaceName);
138
139impl FromStr for InterfaceName {
140    type Err = InvalidInterfaceName;
141
142    /// Attempts to parse a Rust string as a valid Linux interface name.
143    ///
144    /// Extra validation logic ported from [iproute2](https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/lib/utils.c#n827)
145    fn from_str(name: &str) -> Result<Self, InvalidInterfaceName> {
146        let len = name.len();
147        if len == 0 {
148            return Err(InvalidInterfaceName::Empty);
149        }
150
151        // Ensure its short enough to include a trailing NUL
152        if len > (libc::IFNAMSIZ - 1) {
153            return Err(InvalidInterfaceName::TooLong);
154        }
155
156        let mut buf = [c_char::default(); libc::IFNAMSIZ];
157        // Check for interior NULs and other invalid characters.
158        for (out, b) in buf.iter_mut().zip(name.as_bytes().iter()) {
159            if *b == 0 || *b == b'/' || b.is_ascii_whitespace() {
160                return Err(InvalidInterfaceName::InvalidChars);
161            }
162
163            *out = *b as c_char;
164        }
165
166        Ok(Self(buf))
167    }
168}
169
170impl InterfaceName {
171    /// Returns a human-readable form of the device name.
172    ///
173    /// Only use this when the interface name was constructed from a Rust string.
174    pub fn as_str_lossy(&self) -> Cow<'_, str> {
175        // SAFETY: These are C strings coming from wgctrl, so they are correctly NUL terminated.
176        unsafe { CStr::from_ptr(self.0.as_ptr()) }.to_string_lossy()
177    }
178
179    #[cfg(target_os = "linux")]
180    /// Returns a pointer to the inner byte buffer for FFI calls.
181    pub fn as_ptr(&self) -> *const c_char {
182        self.0.as_ptr()
183    }
184}
185
186impl fmt::Debug for InterfaceName {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.write_str(&self.as_str_lossy())
189    }
190}
191
192impl fmt::Display for InterfaceName {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.write_str(&self.as_str_lossy())
195    }
196}
197
198/// An interface name was bad.
199#[derive(Debug, PartialEq, Eq)]
200pub enum InvalidInterfaceName {
201    /// Provided name was longer then the interface name length limit
202    /// of the system.
203    TooLong,
204
205    // These checks are done in the kernel as well, but no reason to let bad names
206    // get that far: https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/lib/utils.c?id=1f420318bda3cc62156e89e1b56d60cc744b48ad#n827.
207    /// Interface name was an empty string.
208    Empty,
209    /// Interface name contained a nul, `/` or whitespace character.
210    InvalidChars,
211}
212
213impl fmt::Display for InvalidInterfaceName {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            Self::TooLong => write!(
217                f,
218                "interface name longer than system max of {} chars",
219                libc::IFNAMSIZ
220            ),
221            Self::Empty => f.write_str("an empty interface name was provided"),
222            Self::InvalidChars => f.write_str("interface name contained slash or space characters"),
223        }
224    }
225}
226
227impl From<InvalidInterfaceName> for std::io::Error {
228    fn from(e: InvalidInterfaceName) -> Self {
229        std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
230    }
231}
232
233impl std::error::Error for InvalidInterfaceName {}
234
235impl Device {
236    /// Enumerates all WireGuard interfaces currently present in the system,
237    /// both with kernel and userspace backends.
238    ///
239    /// You can use [`get_by_name`](DeviceInfo::get_by_name) to retrieve more
240    /// detailed information on each interface.
241    pub fn list(backend: Backend) -> Result<Vec<InterfaceName>, std::io::Error> {
242        match backend {
243            #[cfg(target_os = "linux")]
244            Backend::Kernel => backends::kernel::enumerate(),
245            #[cfg(target_os = "openbsd")]
246            Backend::OpenBSD => backends::openbsd::enumerate(),
247            Backend::Userspace => backends::userspace::enumerate(),
248        }
249    }
250
251    pub fn get(name: &InterfaceName, backend: Backend) -> Result<Self, std::io::Error> {
252        match backend {
253            #[cfg(target_os = "linux")]
254            Backend::Kernel => backends::kernel::get_by_name(name),
255            #[cfg(target_os = "openbsd")]
256            Backend::OpenBSD => backends::openbsd::get_by_name(name),
257            Backend::Userspace => backends::userspace::get_by_name(name),
258        }
259    }
260
261    pub fn delete(self) -> Result<(), std::io::Error> {
262        match self.backend {
263            #[cfg(target_os = "linux")]
264            Backend::Kernel => backends::kernel::delete_interface(&self.name),
265            #[cfg(target_os = "openbsd")]
266            Backend::OpenBSD => backends::openbsd::delete_interface(&self.name),
267            Backend::Userspace => backends::userspace::delete_interface(&self.name),
268        }
269    }
270}
271
272/// Builds and represents a configuration that can be applied to a WireGuard interface.
273///
274/// This is the primary way of changing the settings of an interface.
275///
276/// Note that if an interface exists, the configuration is applied _on top_ of the existing
277/// settings, and missing parts are not overwritten or set to defaults.
278///
279/// If this is not what you want, use [`delete_interface`](delete_interface)
280/// to remove the interface entirely before applying the new configuration.
281///
282/// # Example
283/// ```rust
284/// # use wireguard_control::*;
285/// # use std::net::AddrParseError;
286/// # fn try_main() -> Result<(), AddrParseError> {
287/// let our_keypair = KeyPair::generate();
288/// let peer_keypair = KeyPair::generate();
289/// let server_addr = "192.168.1.1:51820".parse()?;
290///
291/// DeviceUpdate::new()
292///     .set_keypair(our_keypair)
293///     .replace_peers()
294///     .add_peer_with(&peer_keypair.public, |peer| {
295///         peer.set_endpoint(server_addr)
296///             .replace_allowed_ips()
297///             .allow_all_ips()
298///     }).apply(&"wg-example".parse().unwrap(), Backend::Userspace);
299///
300/// println!("Send these keys to your peer: {:#?}", peer_keypair);
301///
302/// # Ok(())
303/// # }
304/// # fn main() { try_main(); }
305/// ```
306#[derive(Debug, PartialEq, Eq, Clone)]
307pub struct DeviceUpdate {
308    pub(crate) public_key: Option<Key>,
309    pub(crate) private_key: Option<Key>,
310    pub(crate) fwmark: Option<u32>,
311    pub(crate) listen_port: Option<u16>,
312    pub(crate) peers: Vec<PeerConfigBuilder>,
313    pub(crate) replace_peers: bool,
314}
315
316impl DeviceUpdate {
317    /// Creates a new `DeviceConfigBuilder` that does nothing when applied.
318    #[must_use]
319    pub fn new() -> Self {
320        DeviceUpdate {
321            public_key: None,
322            private_key: None,
323            fwmark: None,
324            listen_port: None,
325            peers: vec![],
326            replace_peers: false,
327        }
328    }
329
330    /// Sets a new keypair to be applied to the interface.
331    ///
332    /// This is a convenience method that simply wraps
333    /// [`set_public_key`](DeviceConfigBuilder::set_public_key)
334    /// and [`set_private_key`](DeviceConfigBuilder::set_private_key).
335    #[must_use]
336    pub fn set_keypair(self, keypair: KeyPair) -> Self {
337        self.set_public_key(keypair.public)
338            .set_private_key(keypair.private)
339    }
340
341    /// Specifies a new public key to be applied to the interface.
342    #[must_use]
343    pub fn set_public_key(mut self, key: Key) -> Self {
344        self.public_key = Some(key);
345        self
346    }
347
348    /// Specifies that the public key for this interface should be unset.
349    #[must_use]
350    pub fn unset_public_key(self) -> Self {
351        self.set_public_key(Key::zero())
352    }
353
354    /// Sets a new private key to be applied to the interface.
355    #[must_use]
356    pub fn set_private_key(mut self, key: Key) -> Self {
357        self.private_key = Some(key);
358        self
359    }
360
361    /// Specifies that the private key for this interface should be unset.
362    #[must_use]
363    pub fn unset_private_key(self) -> Self {
364        self.set_private_key(Key::zero())
365    }
366
367    /// Specifies the fwmark value that should be applied to packets coming from the interface.
368    #[must_use]
369    pub fn set_fwmark(mut self, fwmark: u32) -> Self {
370        self.fwmark = Some(fwmark);
371        self
372    }
373
374    /// Specifies that fwmark should not be set on packets from the interface.
375    #[must_use]
376    pub fn unset_fwmark(self) -> Self {
377        self.set_fwmark(0)
378    }
379
380    /// Specifies the port to listen for incoming packets on.
381    ///
382    /// This is useful for a server configuration that listens on a fixed endpoint.
383    #[must_use]
384    pub fn set_listen_port(mut self, port: u16) -> Self {
385        self.listen_port = Some(port);
386        self
387    }
388
389    /// Specifies that a random port should be used for incoming packets.
390    ///
391    /// This is probably what you want in client configurations.
392    #[must_use]
393    pub fn randomize_listen_port(self) -> Self {
394        self.set_listen_port(0)
395    }
396
397    /// Specifies a new peer configuration to be added to the interface.
398    ///
399    /// See [`PeerConfigBuilder`](PeerConfigBuilder) for details on building
400    /// peer configurations. This method can be called more than once, and all
401    /// peers will be added to the configuration.
402    #[must_use]
403    pub fn add_peer(mut self, peer: PeerConfigBuilder) -> Self {
404        self.peers.push(peer);
405        self
406    }
407
408    /// Specifies a new peer configuration using a builder function.
409    ///
410    /// This is simply a convenience method to make adding peers more fluent.
411    /// This method can be called more than once, and all peers will be added
412    /// to the configuration.
413    #[must_use]
414    pub fn add_peer_with(
415        self,
416        pubkey: &Key,
417        builder: impl Fn(PeerConfigBuilder) -> PeerConfigBuilder,
418    ) -> Self {
419        self.add_peer(builder(PeerConfigBuilder::new(pubkey)))
420    }
421
422    /// Specifies multiple peer configurations to be added to the interface.
423    #[must_use]
424    pub fn add_peers(mut self, peers: &[PeerConfigBuilder]) -> Self {
425        self.peers.extend_from_slice(peers);
426        self
427    }
428
429    /// Specifies that the peer configurations in this `DeviceConfigBuilder` should
430    /// replace the existing configurations on the interface, not modify or append to them.
431    #[must_use]
432    pub fn replace_peers(mut self) -> Self {
433        self.replace_peers = true;
434        self
435    }
436
437    /// Specifies that the peer with this public key should be removed from the interface.
438    #[must_use]
439    pub fn remove_peer_by_key(self, public_key: &Key) -> Self {
440        let mut peer = PeerConfigBuilder::new(public_key);
441        peer.remove_me = true;
442        self.add_peer(peer)
443    }
444
445    /// Build and apply the configuration to a WireGuard interface by name.
446    ///
447    /// An interface with the provided name will be created if one does not exist already.
448    pub fn apply(self, iface: &InterfaceName, backend: Backend) -> io::Result<()> {
449        match backend {
450            #[cfg(target_os = "linux")]
451            Backend::Kernel => backends::kernel::apply(&self, iface),
452            #[cfg(target_os = "openbsd")]
453            Backend::OpenBSD => backends::openbsd::apply(&self, iface),
454            Backend::Userspace => backends::userspace::apply(&self, iface),
455        }
456    }
457}
458
459impl Default for DeviceUpdate {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use crate::{DeviceUpdate, InterfaceName, InvalidInterfaceName, KeyPair, PeerConfigBuilder};
468
469    const TEST_INTERFACE: &str = "wgctrl-test";
470    use super::*;
471
472    #[test]
473    fn test_add_peers() {
474        if unsafe { libc::getuid() } != 0 {
475            return;
476        }
477
478        let keypairs: Vec<_> = (0..10).map(|_| KeyPair::generate()).collect();
479        let mut builder = DeviceUpdate::new();
480        for keypair in &keypairs {
481            builder = builder.add_peer(PeerConfigBuilder::new(&keypair.public))
482        }
483        let interface = TEST_INTERFACE.parse().unwrap();
484        builder.apply(&interface, Backend::Userspace).unwrap();
485
486        let device = Device::get(&interface, Backend::Userspace).unwrap();
487
488        for keypair in &keypairs {
489            assert!(device
490                .peers
491                .iter()
492                .any(|p| p.config.public_key == keypair.public));
493        }
494
495        device.delete().unwrap();
496    }
497
498    #[test]
499    fn test_interface_names() {
500        assert_eq!(
501            "wg-01".parse::<InterfaceName>().unwrap().as_str_lossy(),
502            "wg-01"
503        );
504        assert!("longer-nul\0".parse::<InterfaceName>().is_err());
505
506        let invalid_names = &[
507            ("", InvalidInterfaceName::Empty),          // Empty Rust string
508            ("\0", InvalidInterfaceName::InvalidChars), // Empty C string
509            ("ifname\0nul", InvalidInterfaceName::InvalidChars), // Contains interior NUL
510            ("if name", InvalidInterfaceName::InvalidChars), // Contains a space
511            ("ifna/me", InvalidInterfaceName::InvalidChars), // Contains a slash
512            ("if na/me", InvalidInterfaceName::InvalidChars), // Contains a space and slash
513            ("interfacelongname", InvalidInterfaceName::TooLong), // Too long
514        ];
515
516        for (name, expected) in invalid_names {
517            assert!(name.parse::<InterfaceName>().as_ref() == Err(expected))
518        }
519    }
520}