1use std::net::{IpAddr, SocketAddr};
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12use thiserror::Error;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct HqWgAllowedIp {
17 pub address: IpAddr,
18 pub prefix_len: u8,
19}
20
21impl HqWgAllowedIp {
22 pub fn host(address: IpAddr) -> Self {
23 Self {
24 address,
25 prefix_len: match address {
26 IpAddr::V4(_) => 32,
27 IpAddr::V6(_) => 128,
28 },
29 }
30 }
31
32 pub fn parse(text: &str) -> Option<Self> {
33 let (addr, prefix) = text.split_once('/')?;
34 let address: IpAddr = addr.parse().ok()?;
35 let prefix_len: u8 = prefix.parse().ok()?;
36 let max = match address {
37 IpAddr::V4(_) => 32,
38 IpAddr::V6(_) => 128,
39 };
40 if prefix_len > max {
41 return None;
42 }
43 Some(Self { address, prefix_len })
44 }
45
46 fn cidr(&self) -> String {
47 format!("{}/{}", self.address, self.prefix_len)
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct HqWgPeer {
54 pub public_key: String,
55 pub endpoint: Option<SocketAddr>,
57 pub allowed_ips: Vec<HqWgAllowedIp>,
58 pub persistent_keepalive_secs: u16,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct HqWgClientConfig {
64 pub interface_name: String,
65 pub private_key_path: PathBuf,
66 pub listen_port: u16,
67 pub hq_tunnel_address: IpAddr,
68 pub c2_tunnel_address: IpAddr,
69 pub control_port: u16,
70 pub drain_port: u16,
72 pub peers: Vec<HqWgPeer>,
73}
74
75impl HqWgClientConfig {
76 pub fn validate(&self) -> Result<(), HqWgClientError> {
77 if !valid_iface(&self.interface_name) {
78 return Err(HqWgClientError::InterfaceName);
79 }
80 if self.private_key_path.as_os_str().is_empty() {
81 return Err(HqWgClientError::PrivateKeyPath);
82 }
83 if self.listen_port != 0 {
84 return Err(HqWgClientError::ListenPort(self.listen_port));
85 }
86 if self.control_port == 0 {
87 return Err(HqWgClientError::ControlPort);
88 }
89 if self.drain_port == 0 {
90 return Err(HqWgClientError::DrainPort);
91 }
92 if self.hq_tunnel_address.is_unspecified()
93 || self.c2_tunnel_address.is_unspecified()
94 || self.hq_tunnel_address == self.c2_tunnel_address
95 || std::mem::discriminant(&self.hq_tunnel_address)
96 != std::mem::discriminant(&self.c2_tunnel_address)
97 {
98 return Err(HqWgClientError::TunnelAddress);
99 }
100 if self.peers.len() != 1 {
101 return Err(HqWgClientError::PeerCount(self.peers.len()));
102 }
103 let peer = &self.peers[0];
104 if peer.public_key.is_empty() {
105 return Err(HqWgClientError::PeerKey);
106 }
107 match peer.endpoint {
108 Some(endpoint) if !endpoint.ip().is_unspecified() && endpoint.port() != 0 => {}
109 _ => return Err(HqWgClientError::MissingEndpoint),
110 }
111 if peer.persistent_keepalive_secs == 0 {
112 return Err(HqWgClientError::KeepaliveDisabled);
113 }
114 if !allowed_ips_are_only_c2(&peer.allowed_ips, self.c2_tunnel_address) {
115 return Err(HqWgClientError::AllowedIps);
116 }
117 Ok(())
118 }
119
120 pub fn control_addr(&self) -> Result<SocketAddr, HqWgClientError> {
122 self.validate()?;
123 Ok(SocketAddr::new(self.c2_tunnel_address, self.control_port))
124 }
125
126 pub fn drain_addr(&self) -> Result<SocketAddr, HqWgClientError> {
128 self.validate()?;
129 Ok(SocketAddr::new(self.c2_tunnel_address, self.drain_port))
130 }
131}
132
133fn allowed_ips_are_only_c2(allowed: &[HqWgAllowedIp], c2: IpAddr) -> bool {
134 let Some(one) = allowed.first() else {
135 return false;
136 };
137 if allowed.len() != 1 || one.address != c2 {
138 return false;
139 }
140 match c2 {
141 IpAddr::V4(_) => one.prefix_len == 32,
142 IpAddr::V6(_) => one.prefix_len == 128,
143 }
144}
145
146fn valid_iface(name: &str) -> bool {
147 let bytes = name.as_bytes();
148 (1..16).contains(&bytes.len())
149 && bytes.iter().all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_' || *byte == b'-')
150}
151
152#[derive(Debug, Error)]
153pub enum HqWgClientError {
154 #[error("HQ WireGuard client is missing the C2 public endpoint")]
155 MissingEndpoint,
156 #[error("HQ WireGuard listen port must be 0, got {0}")]
157 ListenPort(u16),
158 #[error("HQ WireGuard allowed IPs must be only the single C2 tunnel address")]
159 AllowedIps,
160 #[error("HQ WireGuard client must have exactly one peer (C2), got {0}")]
161 PeerCount(usize),
162 #[error("HQ WireGuard persistent keepalive must be on")]
163 KeepaliveDisabled,
164 #[error("HQ WireGuard interface name is invalid")]
165 InterfaceName,
166 #[error("HQ WireGuard private key path is empty")]
167 PrivateKeyPath,
168 #[error("HQ WireGuard tunnel addresses are invalid")]
169 TunnelAddress,
170 #[error("HQ WireGuard control port must be nonzero")]
171 ControlPort,
172 #[error("HQ WireGuard drain port must be nonzero")]
173 DrainPort,
174 #[error("HQ WireGuard peer public key is empty")]
175 PeerKey,
176 #[error("kernel wireguard interface was not created: {0}")]
177 InterfaceCreate(String),
178 #[error("kernel wireguard setup failed: {0}")]
179 Setup(String),
180}
181
182pub fn bring_up_hq_wireguard(config: &HqWgClientConfig) -> Result<SocketAddr, HqWgClientError> {
186 let control = config.control_addr()?;
187 let peer = &config.peers[0];
188 let endpoint = peer.endpoint.expect("validate requires an endpoint");
189 let key_path = config.private_key_path.as_path();
190 require_key_file(key_path)?;
191
192 create_wireguard_iface(&config.interface_name)?;
193 let configured = configure_wireguard(config, peer, endpoint);
194 if let Err(error) = configured {
195 delete_iface(&config.interface_name);
196 return Err(error);
197 }
198 Ok(control)
199}
200
201fn require_key_file(path: &Path) -> Result<(), HqWgClientError> {
202 let meta = std::fs::metadata(path).map_err(|error| {
203 HqWgClientError::Setup(format!("private key path {}: {error}", path.display()))
204 })?;
205 if !meta.is_file() || meta.len() == 0 {
206 return Err(HqWgClientError::Setup(format!(
207 "private key path {} is not a nonempty file",
208 path.display()
209 )));
210 }
211 Ok(())
212}
213
214fn create_wireguard_iface(name: &str) -> Result<(), HqWgClientError> {
215 let output = Command::new("ip")
216 .args(["link", "add", "dev", name, "type", "wireguard"])
217 .output()
218 .map_err(|error| {
219 HqWgClientError::InterfaceCreate(format!("ip link add type wireguard: {error}"))
220 })?;
221 if output.status.success() {
222 return Ok(());
223 }
224 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
225 Err(HqWgClientError::InterfaceCreate(format!(
226 "ip link add dev {name} type wireguard failed: {stderr}"
227 )))
228}
229
230fn configure_wireguard(
231 config: &HqWgClientConfig,
232 peer: &HqWgPeer,
233 endpoint: SocketAddr,
234) -> Result<(), HqWgClientError> {
235 let name = &config.interface_name;
236 let allowed = peer.allowed_ips[0].cidr();
237 command_ok(
238 "wg",
239 &[
240 "set",
241 name,
242 "listen-port",
243 "0",
244 "private-key",
245 &config.private_key_path.display().to_string(),
246 "peer",
247 &peer.public_key,
248 "endpoint",
249 &endpoint.to_string(),
250 "allowed-ips",
251 &allowed,
252 "persistent-keepalive",
253 &peer.persistent_keepalive_secs.to_string(),
254 ],
255 )?;
256 let prefix = match config.hq_tunnel_address {
257 IpAddr::V4(_) => 32,
258 IpAddr::V6(_) => 128,
259 };
260 let local = format!("{}/{}", config.hq_tunnel_address, prefix);
261 command_ok("ip", &["addr", "replace", &local, "dev", name])?;
262 command_ok("ip", &["link", "set", "up", "dev", name])?;
263 let route = format!("{}/{}", config.c2_tunnel_address, if prefix == 32 { 32 } else { 128 });
264 command_ok("ip", &["route", "replace", &route, "dev", name])?;
267 Ok(())
268}
269
270fn command_ok(program: &str, args: &[&str]) -> Result<(), HqWgClientError> {
271 let output = Command::new(program).args(args).output().map_err(|error| {
272 HqWgClientError::Setup(format!("{program}: {error}"))
273 })?;
274 if output.status.success() {
275 return Ok(());
276 }
277 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
278 Err(HqWgClientError::Setup(format!(
279 "{program} {} failed: {stderr}",
280 args.join(" ")
281 )))
282}
283
284fn delete_iface(name: &str) {
285 let _ = Command::new("ip").args(["link", "del", "dev", name]).status();
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn sample() -> HqWgClientConfig {
293 HqWgClientConfig {
294 interface_name: "wg-hq".to_owned(),
295 private_key_path: PathBuf::from("/tmp/hq.priv"),
296 listen_port: 0,
297 hq_tunnel_address: "10.88.1.3".parse().unwrap(),
298 c2_tunnel_address: "10.88.1.2".parse().unwrap(),
299 control_port: 27441,
300 drain_port: 19002,
301 peers: vec![HqWgPeer {
302 public_key: "pyjbO05QwAyerdhvLEogM9JRv8P8Im52v2b3P1KHfwM=".to_owned(),
303 endpoint: Some("192.168.78.2:51820".parse().unwrap()),
304 allowed_ips: vec![HqWgAllowedIp::host("10.88.1.2".parse().unwrap())],
305 persistent_keepalive_secs: 25,
306 }],
307 }
308 }
309
310 #[test]
311 fn single_c2_tunnel_host_is_accepted() {
312 let config = sample();
313 assert!(config.validate().is_ok());
314 assert_eq!(
315 config.control_addr().unwrap(),
316 "10.88.1.2:27441".parse().unwrap()
317 );
318 }
319
320 #[test]
321 fn missing_endpoint_is_rejected() {
322 let mut config = sample();
323 config.peers[0].endpoint = None;
324 assert!(matches!(config.validate(), Err(HqWgClientError::MissingEndpoint)));
325 config.peers[0].endpoint = Some("0.0.0.0:51820".parse().unwrap());
326 assert!(matches!(config.validate(), Err(HqWgClientError::MissingEndpoint)));
327 }
328
329 #[test]
330 fn listen_port_other_than_zero_is_rejected() {
331 let mut config = sample();
332 config.listen_port = 51820;
333 assert!(matches!(config.validate(), Err(HqWgClientError::ListenPort(51820))));
334 }
335
336 #[test]
337 fn allowed_ips_other_than_the_c2_tunnel_address_are_rejected() {
338 let node: IpAddr = "10.88.1.1".parse().unwrap();
339 let c2: IpAddr = "10.88.1.2".parse().unwrap();
340 let cases = [
341 vec![HqWgAllowedIp { address: "10.88.1.0".parse().unwrap(), prefix_len: 24 }],
342 vec![HqWgAllowedIp { address: c2, prefix_len: 24 }],
343 vec![HqWgAllowedIp::host(c2), HqWgAllowedIp::host(node)],
344 vec![HqWgAllowedIp::host(node)],
345 vec![],
346 ];
347 for allowed in cases {
348 let mut config = sample();
349 config.peers[0].allowed_ips = allowed;
350 assert!(
351 matches!(config.validate(), Err(HqWgClientError::AllowedIps)),
352 "allowed-ips must be only 10.88.1.2/32"
353 );
354 }
355 }
356
357 #[test]
358 fn drain_addr_is_tunnel_port() {
359 let cfg = sample();
360 assert_eq!(cfg.drain_addr().unwrap().to_string(), "10.88.1.2:19002");
361 }
362}