Skip to main content

zlayer_overlay/
config.rs

1//! Overlay network configuration
2
3#[cfg(feature = "nat")]
4use crate::nat::NatConfig;
5use serde::{Deserialize, Serialize};
6use std::net::{IpAddr, Ipv4Addr, SocketAddr};
7use std::path::PathBuf;
8use std::time::Duration;
9
10/// Overlay network configuration
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct OverlayConfig {
13    /// Local overlay endpoint (`WireGuard` protocol)
14    pub local_endpoint: SocketAddr,
15
16    /// Private key (x25519)
17    pub private_key: String,
18
19    /// Public key (derived from private key)
20    #[serde(default = "OverlayConfig::default_public_key")]
21    pub public_key: String,
22
23    /// Overlay network CIDR (supports both IPv4 e.g. "10.0.0.0/8" and IPv6 e.g. "`fd00::/48`")
24    ///
25    /// Historically stores the per-node slice / host IP (e.g. `10.200.0.0/28`
26    /// or `10.200.0.1/32`) that the local TUN/Wintun adapter is assigned.
27    /// It is *not* the full cluster CIDR — use [`Self::cluster_cidr`] for that.
28    #[serde(default = "OverlayConfig::default_cidr")]
29    pub overlay_cidr: String,
30
31    /// Full cluster CIDR (e.g. `10.200.0.0/16`).
32    ///
33    /// Used on Windows to install a catch-all host route pointing the
34    /// entire cluster range at the Wintun adapter so traffic to remote-node
35    /// container IPs flows through the overlay (HCN auto-installs the more
36    /// specific local /28 → vSwitch route, and longest-prefix-match routes
37    /// local traffic to the vSwitch). `None` on pre-cluster-CIDR configs;
38    /// callers should fall back to skipping the route install in that case.
39    #[serde(default)]
40    pub cluster_cidr: Option<String>,
41
42    /// Peer discovery interval
43    #[serde(default = "OverlayConfig::default_discovery")]
44    pub peer_discovery_interval: Duration,
45
46    /// NAT traversal configuration (requires "nat" feature)
47    #[cfg(feature = "nat")]
48    #[serde(default)]
49    pub nat: NatConfig,
50
51    /// Directory containing per-interface `WireGuard` UAPI sockets
52    /// (`<dir>/<interface_name>.sock`). Defaults to `/var/run/wireguard`
53    /// on Linux for `wg(8)` interop; overridden to `{data_dir}/run/wireguard`
54    /// by the daemon when running with a non-default `--data-dir` to keep
55    /// a test/dev daemon hermetic.
56    #[serde(default = "OverlayConfig::default_uapi_sock_dir")]
57    pub uapi_sock_dir: PathBuf,
58
59    /// MTU applied to the overlay tunnel interface.
60    ///
61    /// Defaults to `1420`: the standard `WireGuard` tunnel MTU (1500-byte
62    /// underlay MTU minus 80 bytes of `WireGuard` encapsulation overhead).
63    /// Matches the Windows Wintun default in `transport.rs`.
64    ///
65    /// Applied to the TUN interface on Linux/macOS at configure time. On
66    /// Windows, Wintun exposes no per-adapter MTU setter, so this value is
67    /// advisory there (IP Helper may override it). Lowering it matters when
68    /// running `WireGuard`-in-`WireGuard` — e.g. the overlay riding over
69    /// another mesh such as netbird — where stacked encapsulation shrinks
70    /// the usable payload and an over-large MTU causes silent blackholing
71    /// of oversized packets that can't be fragmented.
72    #[serde(default = "default_mtu")]
73    pub mtu: u32,
74}
75
76/// Default overlay tunnel MTU: 1500-byte underlay minus 80 bytes of
77/// `WireGuard` encapsulation overhead.
78fn default_mtu() -> u32 {
79    1420
80}
81
82impl OverlayConfig {
83    fn default_public_key() -> String {
84        String::new()
85    }
86
87    fn default_cidr() -> String {
88        "10.0.0.0/8".to_string()
89    }
90
91    fn default_discovery() -> Duration {
92        Duration::from_secs(30)
93    }
94
95    /// Platform-default `WireGuard` UAPI socket directory.
96    ///
97    /// Linux: `/var/run/wireguard` (FHS, matches `wg(8)`).
98    /// macOS / Windows: `/var/run/wireguard` is the historical literal
99    /// the transport used; the daemon overrides this to a data-dir-aware
100    /// path via [`zlayer_paths::ZLayerDirs::wireguard`] when a non-default
101    /// `--data-dir` is in play.
102    fn default_uapi_sock_dir() -> PathBuf {
103        PathBuf::from("/var/run/wireguard")
104    }
105}
106
107impl Default for OverlayConfig {
108    fn default() -> Self {
109        Self {
110            local_endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51820),
111            private_key: String::new(),
112            public_key: String::new(),
113            overlay_cidr: "10.0.0.0/8".to_string(),
114            cluster_cidr: None,
115            peer_discovery_interval: Duration::from_secs(30),
116            #[cfg(feature = "nat")]
117            nat: NatConfig::default(),
118            uapi_sock_dir: Self::default_uapi_sock_dir(),
119            mtu: default_mtu(),
120        }
121    }
122}
123
124/// Peer information
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126pub struct PeerInfo {
127    /// Peer public key
128    pub public_key: String,
129
130    /// Endpoint address
131    pub endpoint: SocketAddr,
132
133    /// Allowed IPs
134    pub allowed_ips: String,
135
136    /// Persistent keepalive interval
137    pub persistent_keepalive_interval: Duration,
138}
139
140impl PeerInfo {
141    /// Create a new peer info
142    #[must_use]
143    pub fn new(
144        public_key: String,
145        endpoint: SocketAddr,
146        allowed_ips: &str,
147        persistent_keepalive_interval: Duration,
148    ) -> Self {
149        Self {
150            public_key,
151            endpoint,
152            allowed_ips: allowed_ips.to_string(),
153            persistent_keepalive_interval,
154        }
155    }
156
157    /// Create a peer config block (`WireGuard` protocol format)
158    #[must_use]
159    pub fn to_peer_config(&self) -> String {
160        format!(
161            "[Peer]\n\
162             PublicKey = {}\n\
163             Endpoint = {}\n\
164             AllowedIPs = {}\n\
165             PersistentKeepalive = {}\n",
166            self.public_key,
167            self.endpoint,
168            self.allowed_ips,
169            self.persistent_keepalive_interval.as_secs()
170        )
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_peer_info_to_peer_config() {
180        let peer = PeerInfo::new(
181            "public_key_here".to_string(),
182            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 51820),
183            "10.0.0.2/32",
184            Duration::from_secs(25),
185        );
186
187        let config = peer.to_peer_config();
188        assert!(config.contains("PublicKey = public_key_here"));
189        assert!(config.contains("Endpoint = 192.168.1.1:51820"));
190    }
191
192    #[test]
193    fn test_overlay_config_default() {
194        let config = OverlayConfig::default();
195        assert_eq!(config.local_endpoint.port(), 51820);
196        assert_eq!(config.overlay_cidr, "10.0.0.0/8");
197    }
198
199    #[test]
200    fn test_peer_info_to_peer_config_v6() {
201        use std::net::Ipv6Addr;
202
203        let peer = PeerInfo::new(
204            "public_key_here".to_string(),
205            SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 51820),
206            "fd00::2/128",
207            Duration::from_secs(25),
208        );
209
210        let config = peer.to_peer_config();
211        assert!(config.contains("PublicKey = public_key_here"));
212        assert!(config.contains("Endpoint = [::1]:51820"));
213        assert!(config.contains("AllowedIPs = fd00::2/128"));
214    }
215
216    #[test]
217    fn test_overlay_config_accepts_ipv6_cidr() {
218        let config = OverlayConfig {
219            overlay_cidr: "fd00:200::/48".to_string(),
220            ..OverlayConfig::default()
221        };
222        assert_eq!(config.overlay_cidr, "fd00:200::/48");
223    }
224
225    #[test]
226    fn test_overlay_config_default_mtu() {
227        let config = OverlayConfig::default();
228        assert_eq!(config.mtu, 1420);
229    }
230
231    #[test]
232    fn test_overlay_config_mtu_serde_round_trip() {
233        let config = OverlayConfig {
234            mtu: 1280,
235            ..OverlayConfig::default()
236        };
237        let json = serde_json::to_string(&config).expect("serialize");
238        assert!(json.contains("\"mtu\":1280"));
239        let decoded: OverlayConfig = serde_json::from_str(&json).expect("deserialize");
240        assert_eq!(decoded, config);
241        assert_eq!(decoded.mtu, 1280);
242    }
243
244    #[test]
245    fn test_overlay_config_missing_mtu_defaults() {
246        // An on-disk config written before the `mtu` field existed must
247        // still deserialize cleanly and pick up the 1420 default.
248        let json = r#"{
249            "local_endpoint": "0.0.0.0:51820",
250            "private_key": ""
251        }"#;
252        let config: OverlayConfig = serde_json::from_str(json).expect("deserialize without mtu");
253        assert_eq!(config.mtu, 1420);
254    }
255}