1use std::net::{Ipv4Addr, SocketAddrV4};
92use std::path::Path;
93use std::sync::Arc;
94
95use anyhow::{bail, Context, Result};
96use wg_appliance_core::dataplane::{self, PeerSpec, TunnelHandle, TunnelSpec};
97use wg_appliance_core::ini;
98use wg_appliance_core::keys;
99use wg_appliance_core::tun::{TunDevice, DEFAULT_MTU};
100
101
102
103const KEY_LEN: usize = keys::KEY_LEN;
105
106pub const CONFIG_FILE: &str = "wg0.conf";
108
109pub const KEY_FILE: &str = "wg0.key";
115
116pub const BOOT_PARAM: &str = "GUNNAR_WG";
134
135pub fn boot_param() -> Option<String> {
148 std::env::var(BOOT_PARAM)
149 .ok()
150 .filter(|s| !s.trim().is_empty())
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct Peer {
158 pub public_key: [u8; KEY_LEN],
160 pub preshared_key: Option<[u8; KEY_LEN]>,
163 pub allowed_ips: Vec<(Ipv4Addr, u8)>,
166 pub endpoint: Option<SocketAddrV4>,
169 pub persistent_keepalive: Option<u16>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct WgConfig {
176 pub interface: String,
179 pub private_key: [u8; KEY_LEN],
181 pub address: Ipv4Addr,
183 pub prefix: u8,
185 pub listen_port: Option<u16>,
188 pub peers: Vec<Peer>,
190}
191
192impl WgConfig {
193 pub fn parse(interface: &str, text: &str) -> Result<WgConfig> {
217 const REFUSED: [&str; 8] = [
219 "dns",
220 "mtu",
221 "table",
222 "preup",
223 "postup",
224 "predown",
225 "postdown",
226 "saveconfig",
227 ];
228
229 #[derive(PartialEq)]
230 enum Section {
231 None,
232 Interface,
233 Peer,
234 }
235
236 let mut section = Section::None;
237 let mut private_key: Option<[u8; KEY_LEN]> = None;
238 let mut cidr: Option<(Ipv4Addr, u8)> = None;
239 let mut listen_port: Option<u16> = None;
240 let mut peers: Vec<Peer> = Vec::new();
241
242 for item in ini::items(text) {
243 let line = item.line();
244 let (key, value) = match item {
245 ini::Item::Section { name, raw_name, .. } => {
246 section = match name.as_str() {
247 "interface" => Section::Interface,
248 "peer" => {
249 peers.push(Peer {
250 public_key: [0; KEY_LEN],
251 preshared_key: None,
252 allowed_ips: Vec::new(),
253 endpoint: None,
254 persistent_keepalive: None,
255 });
256 Section::Peer
257 }
258 _ => bail!("{interface}.conf:{line}: unknown section [{raw_name}]"),
259 };
260 continue;
261 }
262 ini::Item::Malformed { text, .. } => {
263 bail!("{interface}.conf:{line}: {text:?} is not `Key = value`")
264 }
265 ini::Item::Pair { key, value, .. } => (key, value),
266 };
267 if REFUSED.contains(&key.as_str()) {
268 bail!(
269 "{interface}.conf:{line}: `{key}` is a wg-quick key gunnar does NOT implement. \
270 It is refused rather than ignored, because an ignored key reads as one that \
271 was honoured. (PreUp/PostUp/PostDown run commands, and this server spawns \
272 nothing; DNS/MTU/Table are not configured here.)"
273 );
274 }
275 match (§ion, key.as_str()) {
276 (Section::None, _) => {
277 bail!(
278 "{interface}.conf:{line}: `{key}` before any [Interface] or [Peer] section"
279 )
280 }
281 (Section::Interface, "privatekey") => {
282 private_key = Some(
283 parse_key(value)
284 .with_context(|| format!("{interface}.conf:{line}: PrivateKey"))?,
285 );
286 }
287 (Section::Interface, "address") => {
288 cidr = Some(
289 parse_cidr(value)
290 .with_context(|| format!("{interface}.conf:{line}: Address"))?,
291 );
292 }
293 (Section::Interface, "listenport") => {
294 listen_port = Some(value.parse::<u16>().with_context(|| {
295 format!("{interface}.conf:{line}: ListenPort {value:?} is not a port")
296 })?);
297 }
298 (Section::Peer, "publickey") => {
299 let k = parse_key(value)
300 .with_context(|| format!("{interface}.conf:{line}: PublicKey"))?;
301 peers
302 .last_mut()
303 .expect("a [Peer] section pushed one")
304 .public_key = k;
305 }
306 (Section::Peer, "presharedkey") => {
307 let k = parse_key(value)
308 .with_context(|| format!("{interface}.conf:{line}: PresharedKey"))?;
309 peers
310 .last_mut()
311 .expect("a [Peer] section pushed one")
312 .preshared_key = Some(k);
313 }
314 (Section::Peer, "allowedips") => {
315 let mut ips = Vec::new();
316 for one in value.split(',') {
317 let one = one.trim();
318 if one.is_empty() {
319 continue;
320 }
321 ips.push(parse_cidr(one).with_context(|| {
322 format!("{interface}.conf:{line}: AllowedIPs entry {one:?}")
323 })?);
324 }
325 if ips.is_empty() {
326 bail!("{interface}.conf:{line}: AllowedIPs is empty");
327 }
328 peers
329 .last_mut()
330 .expect("a [Peer] section pushed one")
331 .allowed_ips = ips;
332 }
333 (Section::Peer, "endpoint") => {
334 let addr: SocketAddrV4 = value.parse().with_context(|| {
335 format!(
336 "{interface}.conf:{line}: Endpoint {value:?} is not <IPv4>:<port>. A \
337 HOSTNAME is deliberately not accepted: the appliance has no resolver \
338 configured this early in the boot"
339 )
340 })?;
341 peers
342 .last_mut()
343 .expect("a [Peer] section pushed one")
344 .endpoint = Some(addr);
345 }
346 (Section::Peer, "persistentkeepalive") => {
347 let secs: u16 = value.parse().with_context(|| {
348 format!(
349 "{interface}.conf:{line}: PersistentKeepalive {value:?} is not a \
350 number of seconds"
351 )
352 })?;
353 peers
354 .last_mut()
355 .expect("a [Peer] section pushed one")
356 .persistent_keepalive = (secs != 0).then_some(secs);
357 }
358 (_, other) => {
359 bail!("{interface}.conf:{line}: unknown key `{other}` in this section")
360 }
361 }
362 }
363
364 let private_key = private_key
365 .with_context(|| format!("{interface}.conf: [Interface] has no PrivateKey"))?;
366 let (address, prefix) =
367 cidr.with_context(|| format!("{interface}.conf: [Interface] has no Address"))?;
368 if peers.is_empty() {
369 bail!("{interface}.conf: no [Peer] section — a tunnel with no peer reaches nothing");
370 }
371 for (i, peer) in peers.iter().enumerate() {
372 if peer.public_key == [0; KEY_LEN] {
373 bail!("{interface}.conf: [Peer] #{} has no PublicKey", i + 1);
374 }
375 if peer.allowed_ips.is_empty() {
376 bail!(
377 "{interface}.conf: [Peer] #{} has no AllowedIPs — nothing would be routed \
378 into the tunnel for it and nothing accepted from it",
379 i + 1
380 );
381 }
382 }
383 Ok(WgConfig {
384 interface: interface.to_string(),
385 private_key,
386 address,
387 prefix,
388 listen_port,
389 peers,
390 })
391 }
392
393 pub fn parse_boot_param(
409 interface: &str,
410 spec: &str,
411 private_key: [u8; KEY_LEN],
412 ) -> Result<WgConfig> {
413 let (mut addr, mut peer, mut allowed, mut endpoint, mut keepalive) =
414 (None, None, None, None, None);
415 for field in spec.split(',') {
416 let field = field.trim();
417 if field.is_empty() {
418 continue;
419 }
420 let (key, value) = field
421 .split_once('=')
422 .with_context(|| format!("{BOOT_PARAM}: {field:?} is not `key=value`"))?;
423 match key.trim().to_ascii_lowercase().as_str() {
424 "addr" => addr = Some(parse_cidr(value).context("addr=")?),
425 "peer" => peer = Some(parse_key(value).context("peer=")?),
426 "allowed" => {
427 let mut ips = Vec::new();
428 for one in value.split_whitespace() {
431 ips.push(parse_cidr(one).context("allowed=")?);
432 }
433 let mut all: Vec<_> = allowed.unwrap_or_default();
434 all.extend(ips);
435 allowed = Some(all);
436 }
437 "endpoint" => {
438 endpoint = Some(value.parse::<SocketAddrV4>().with_context(|| {
439 format!(
440 "{BOOT_PARAM}: endpoint={value:?} is not <IPv4>:<port>; a hostname \
441 is deliberately not accepted (no resolver this early in the boot)"
442 )
443 })?)
444 }
445 "keepalive" => {
446 let secs: u16 = value.parse().with_context(|| {
447 format!("{BOOT_PARAM}: keepalive={value:?} is not a number of seconds")
448 })?;
449 keepalive = (secs != 0).then_some(secs);
450 }
451 other => bail!(
452 "{BOOT_PARAM}: unknown field `{other}`. This form takes addr, peer, allowed, \
453 endpoint and keepalive — and deliberately NOT a private key, which would be \
454 world-readable in /proc/cmdline"
455 ),
456 }
457 }
458 let (address, prefix) =
459 addr.with_context(|| format!("{BOOT_PARAM}: no `addr=<ip>/<prefix>`"))?;
460 let public_key =
461 peer.with_context(|| format!("{BOOT_PARAM}: no `peer=<hub public key>`"))?;
462 let allowed_ips = allowed
463 .filter(|v: &Vec<_>| !v.is_empty())
464 .with_context(|| format!("{BOOT_PARAM}: no `allowed=<cidr>`"))?;
465 Ok(WgConfig {
466 interface: interface.to_string(),
467 private_key,
468 address,
469 prefix,
470 listen_port: None,
473 peers: vec![Peer {
474 public_key,
475 preshared_key: None,
476 allowed_ips,
477 endpoint,
478 persistent_keepalive: keepalive,
479 }],
480 })
481 }
482
483 pub fn from_data_root(data_root: &Path) -> Result<Option<WgConfig>> {
488 let path = data_root.join(CONFIG_FILE);
489 let text = match std::fs::read_to_string(&path) {
490 Ok(t) => t,
491 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
492 Err(err) => {
493 return Err(err).with_context(|| format!("reading {}", path.display()));
494 }
495 };
496 let interface = Path::new(CONFIG_FILE)
497 .file_stem()
498 .and_then(|s| s.to_str())
499 .expect("CONFIG_FILE is a literal with a stem");
500 Ok(Some(WgConfig::parse(interface, &text)?))
501 }
502
503 pub fn resolve(data_root: &Path) -> Result<Option<WgConfig>> {
527 WgConfig::resolve_with(data_root, boot_param().as_deref())
528 }
529
530 pub fn resolve_with(data_root: &Path, param: Option<&str>) -> Result<Option<WgConfig>> {
536 let file = WgConfig::from_data_root(data_root)?;
537 let param = param.map(str::to_owned);
538 match (file, param) {
539 (Some(_), Some(_)) => bail!(
540 "both {} and {BOOT_PARAM} configure WireGuard. They are two ways to say the same \
541 thing and this refuses to guess which one you meant: delete the file to use the \
542 boot parameter, or drop {BOOT_PARAM} from the command line to use the file",
543 data_root.join(CONFIG_FILE).display()
544 ),
545 (Some(config), None) => Ok(Some(config)),
546 (None, Some(spec)) => {
547 let path = data_root.join(KEY_FILE);
548 let text = std::fs::read_to_string(&path).with_context(|| {
549 format!(
550 "{BOOT_PARAM} is set, so the private key must be seeded at {} — it is \
551 the ONE field that may not ride the world-readable kernel command line",
552 path.display()
553 )
554 })?;
555 let private_key = parse_key(text.trim())
556 .with_context(|| format!("reading the private key from {}", path.display()))?;
557 let interface = Path::new(CONFIG_FILE)
558 .file_stem()
559 .and_then(|s| s.to_str())
560 .expect("CONFIG_FILE is a literal with a stem");
561 Ok(Some(WgConfig::parse_boot_param(
562 interface,
563 spec.trim(),
564 private_key,
565 )?))
566 }
567 (None, None) => Ok(None),
568 }
569 }
570
571 pub fn unrouted_allowed_ips(&self) -> Vec<(Ipv4Addr, u8)> {
577 let mine = masked(self.address, self.prefix);
578 self.peers
579 .iter()
580 .flat_map(|p| p.allowed_ips.iter().copied())
581 .filter(|(addr, prefix)| *prefix < self.prefix || masked(*addr, self.prefix) != mine)
582 .collect()
583 }
584
585 pub fn public_key(&self) -> [u8; KEY_LEN] {
594 keys::public_from_private_bytes(&self.private_key)
595 }
596
597 fn tunnel_spec(&self) -> TunnelSpec {
600 TunnelSpec {
601 private_key: self.private_key,
602 listen_port: self.listen_port,
603 peers: self
604 .peers
605 .iter()
606 .map(|p| PeerSpec {
607 public_key: p.public_key,
608 preshared_key: p.preshared_key,
609 endpoint: p.endpoint,
610 allowed_ips: p.allowed_ips.clone(),
611 persistent_keepalive: p.persistent_keepalive,
612 })
613 .collect(),
614 }
615 }
616}
617
618fn masked(addr: Ipv4Addr, prefix: u8) -> u32 {
620 let bits = u32::from(addr);
621 match prefix {
622 0 => 0,
623 p if p >= 32 => bits,
624 p => bits & !(u32::MAX >> p),
625 }
626}
627
628fn parse_key(text: &str) -> Result<[u8; KEY_LEN]> {
635 keys::decode_key(text).map_err(|e| anyhow::anyhow!("{e}"))
636}
637
638fn parse_cidr(text: &str) -> Result<(Ipv4Addr, u8)> {
640 let (addr, prefix) = text
641 .split_once('/')
642 .with_context(|| format!("{text:?} is not <address>/<prefix>"))?;
643 let addr: Ipv4Addr = addr
644 .trim()
645 .parse()
646 .with_context(|| format!("{addr:?} is not an IPv4 address"))?;
647 let prefix: u8 = prefix
648 .trim()
649 .parse()
650 .with_context(|| format!("{prefix:?} is not a prefix length"))?;
651 if prefix > 32 {
652 bail!("/{prefix} is not a legal IPv4 prefix length");
653 }
654 Ok((addr, prefix))
655}
656
657pub fn generate_keypair() -> ([u8; KEY_LEN], [u8; KEY_LEN]) {
670 keys::generate_keypair()
671}
672
673pub fn keygen_verb(command: gunnar_common::cli::WgCommand) -> Result<()> {
683 let gunnar_common::cli::WgCommand::Keygen { private } = command;
684 let (secret, public) = generate_keypair();
685 let Some(path) = private else {
686 println!("private {}", base64_key(&secret));
687 println!("public {}", base64_key(&public));
688 return Ok(());
689 };
690 if path.exists() {
691 bail!(
692 "{} already exists. This refuses to overwrite an identity: every hub that peers \
693 with this machine allowlists its CURRENT public key, and a peer whose key is \
694 unknown to the other end gets silence rather than an error. Delete the file \
695 deliberately if you really mean to mint a new identity.",
696 path.display()
697 );
698 }
699 write_secret(&path, &format!("{}\n", base64_key(&secret)))
700 .with_context(|| format!("writing the private key to {}", path.display()))?;
701 println!("public {}", base64_key(&public));
702 Ok(())
703}
704
705fn write_secret(path: &Path, body: &str) -> std::io::Result<()> {
710 use std::io::Write as _;
711 use std::os::unix::fs::OpenOptionsExt as _;
712 let mut f = std::fs::OpenOptions::new()
713 .write(true)
714 .create_new(true)
715 .mode(0o600)
716 .open(path)?;
717 f.write_all(body.as_bytes())
718}
719
720#[derive(Clone)]
724pub struct Tunnel {
725 pub interface: String,
727 pub address: Ipv4Addr,
729 pub prefix: u8,
731 pub peers: usize,
733 pub public_key: [u8; KEY_LEN],
744 pub local_port: u16,
746 config: Arc<std::sync::Mutex<WgConfig>>,
750 handle: Arc<std::sync::Mutex<Option<TunnelHandle>>>,
760}
761
762impl std::fmt::Debug for Tunnel {
763 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764 f.debug_struct("Tunnel")
765 .field("interface", &self.interface)
766 .field("address", &self.address)
767 .field("prefix", &self.prefix)
768 .field("peers", &self.peers)
769 .field("public_key", &base64_key(&self.public_key))
770 .field("local_port", &self.local_port)
771 .finish()
772 }
773}
774
775impl Tunnel {
776 pub fn banner_line(&self) -> String {
779 banner_line(
780 &self.interface,
781 self.address,
782 self.prefix,
783 &self.public_key,
784 self.peers,
785 self.local_port,
786 )
787 }
788
789 pub fn handshaken(&self) -> bool {
792 self.handle.lock().unwrap_or_else(|p| p.into_inner()).as_ref().map(TunnelHandle::handshaken).unwrap_or(false)
793 }
794
795 pub fn peer_stats(&self) -> Vec<dataplane::PeerStats> {
797 self.handle.lock().unwrap_or_else(|p| p.into_inner()).as_ref().map(TunnelHandle::peers).unwrap_or_default()
798 }
799
800 pub fn running_config(&self) -> Option<WgConfig> {
802 let up = self.handle.lock().unwrap_or_else(|p| p.into_inner()).is_some();
803 up.then(|| self.config.lock().unwrap_or_else(|p| p.into_inner()).clone())
804 }
805
806 pub fn repoint(&self, wanted: &WgConfig) -> Result<()> {
813 let mut slot = self.handle.lock().unwrap_or_else(|p| p.into_inner());
814 if let Some(mut old) = slot.take() {
815 old.shutdown();
816 }
817 let handle = open_pump(wanted)?;
818 *slot = Some(handle);
819 *self.config.lock().unwrap_or_else(|p| p.into_inner()) = wanted.clone();
820 crate::tunnel_addr::publish(Some(wanted.address));
821 Ok(())
822 }
823}
824
825fn peers_said(c: &WgConfig) -> String {
827 c.peers
828 .iter()
829 .map(|p| format!("{}@{}", &base64_key(&p.public_key)[..8], p.endpoint.map(|e| e.to_string()).unwrap_or_else(|| "-".into())))
830 .collect::<Vec<_>>()
831 .join(", ")
832}
833
834pub fn bring_up(data_root: &Path) -> Result<Option<Tunnel>> {
864 bring_up_with(crate::appliance::gate_enabled(), data_root)
865}
866
867pub fn bring_up_with(gate: bool, data_root: &Path) -> Result<Option<Tunnel>> {
869 if boot_lane(gate, data_root) == BootLane::Ceremony {
870 return ceremony_lane(data_root);
871 }
872 let Some(config) = WgConfig::resolve(data_root)? else {
873 tracing::info!(
874 path = %data_root.join(CONFIG_FILE).display(),
875 "no WireGuard configuration: serving on the outer address only. Either drop a \
876 wg-quick style {CONFIG_FILE} in the data root, or set {BOOT_PARAM} on the kernel \
877 command line and seed {KEY_FILE} beside it. Nothing else is needed — the WireGuard \
878 data plane is compiled into this binary and wants no kernel module."
879 );
880 return Ok(None);
881 };
882 match establish(&config) {
883 Ok(tunnel) => {
884 *LATE_TUNNEL.lock().unwrap_or_else(|p| p.into_inner()) = Some(tunnel.clone());
888 Ok(Some(tunnel))
889 }
890 Err(err) => {
891 tracing::warn!(
892 interface = %config.interface,
893 "the WireGuard configuration parsed and the tunnel would NOT come up: {err:#}. \
894 Serving on the outer address only — no tunnel address exists, so a client \
895 pointed at {} will not connect.",
896 config.address
897 );
898 Ok(None)
899 }
900 }
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub enum BootLane {
906 Ceremony,
908 Config,
910}
911
912pub fn pairing_owns_tunnel(data_root: &Path) -> bool {
915 data_root.join(crate::pairing::SEED_REL).exists()
916 || std::fs::read_to_string(data_root.join(CONFIG_FILE))
917 .map(|t| t.starts_with(crate::pairing::CONF_MARK))
918 .unwrap_or(false)
919}
920
921pub fn boot_lane(gate: bool, data_root: &Path) -> BootLane {
929 if gate && !pairing_owns_tunnel(data_root) {
930 BootLane::Ceremony
931 } else {
932 BootLane::Config
933 }
934}
935
936pub type PairingTunnel = (String, Option<(Ipv4Addr, u8)>, bool);
939
940pub fn pairing_tunnel(data_root: &Path) -> Option<PairingTunnel> {
945 if !pairing_owns_tunnel(data_root) {
946 return None;
947 }
948 let public_b64 = std::fs::read_to_string(data_root.join(KEY_FILE))
949 .ok()
950 .and_then(|k| wg_appliance_core::public_from_private(k.trim()).ok())
951 .unwrap_or_default();
952 let config = WgConfig::resolve_with(data_root, None).ok().flatten();
953 let address = config.as_ref().map(|c| (c.address, c.prefix));
954 let up = config
955 .as_ref()
956 .map(|c| appliance_pid1::net::interface_ipv4(&c.interface).ok().flatten() == Some(c.address))
957 .unwrap_or(false);
958 Some((public_b64, address, up))
959}
960
961fn seeded_lane_conflict(
969 data_root: &Path,
970 seeded_file_present: bool,
971 seeded_param_present: bool,
972) -> Option<String> {
973 if !seeded_file_present && !seeded_param_present {
974 return None;
975 }
976 let named = if seeded_param_present {
977 BOOT_PARAM.to_string()
978 } else {
979 data_root.join(CONFIG_FILE).display().to_string()
980 };
981 let gate = crate::appliance::GATE_ENV;
982 Some(format!(
983 "{gate}=1 arms the identity ceremony, and {named} is also declared. They are two \
984 different answers to what this machine's identity IS, and this refuses to guess: \
985 the ceremony MINTS a key on {} and announces it, the seeded lane takes one you \
986 supplied. Drop the seeded configuration to use the ceremony, or unset {gate} to \
987 keep seeding.",
988 crate::appliance::wg_dir(data_root).display(),
989 ))
990}
991
992fn ceremony_lane(data_root: &Path) -> Result<Option<Tunnel>> {
1002 let seeded_param = boot_param();
1006 if let Some(refusal) = seeded_lane_conflict(
1007 data_root,
1008 data_root.join(CONFIG_FILE).exists(),
1009 seeded_param.is_some(),
1010 ) {
1011 bail!(refusal);
1012 }
1013
1014 let outcome = match crate::appliance::run(data_root, &mut |line| {
1020 println!("{line}");
1021 tracing::info!("{line}");
1022 }) {
1023 Ok(outcome) => {
1024 crate::functional_status(
1025 "gunnar-appliance",
1026 "identity_ready",
1027 true,
1028 &format!(
1029 "pubkey={} minted={} hub={}",
1030 outcome.public_b64,
1031 outcome.minted,
1032 outcome.config.is_some()
1033 ),
1034 );
1035 outcome
1036 }
1037 Err(err) => {
1038 crate::functional_status(
1039 "gunnar-appliance",
1040 "identity_ready",
1041 false,
1042 &format!("{err:#}"),
1043 );
1044 return Err(err);
1045 }
1046 };
1047
1048 let Some(config) = outcome.config else {
1049 return Ok(None);
1050 };
1051 match establish(&config) {
1052 Ok(tunnel) => {
1053 CEREMONY_TUNNEL_UP.store(true, std::sync::atomic::Ordering::SeqCst);
1054 Ok(Some(tunnel))
1055 }
1056 Err(err) => {
1057 tracing::warn!(
1058 interface = %config.interface,
1059 "the hub declaration parsed and the tunnel would NOT come up: {err:#}. \
1060 Serving on the outer address only — the identity above is minted and \
1061 durable, so registering it with the hub is still the right next move.",
1062 );
1063 Ok(None)
1064 }
1065 }
1066}
1067
1068static CEREMONY_TUNNEL_UP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1071
1072static LATE_TUNNEL: std::sync::Mutex<Option<Tunnel>> = std::sync::Mutex::new(None);
1076
1077pub fn config_tunnel_handshaken() -> Option<bool> {
1081 LATE_TUNNEL.lock().unwrap_or_else(|p| p.into_inner()).as_ref().map(Tunnel::handshaken)
1082}
1083
1084pub fn ceremony_tunnel_up() -> bool {
1086 CEREMONY_TUNNEL_UP.load(std::sync::atomic::Ordering::SeqCst)
1087}
1088
1089pub fn raise_after_assignment(data_root: &Path) -> Result<bool> {
1094 if ceremony_tunnel_up() {
1095 return Ok(true);
1096 }
1097 let Some(tunnel) = ceremony_lane(data_root)? else {
1098 return Ok(false);
1099 };
1100 if let Ok(rt) = tokio::runtime::Handle::try_current() {
1101 rt.spawn(handshake_watch(tunnel.clone(), 20, std::time::Duration::from_secs(3)));
1102 }
1103 *LATE_TUNNEL.lock().unwrap_or_else(|p| p.into_inner()) = Some(tunnel);
1104 Ok(true)
1105}
1106
1107#[derive(Debug, Clone, PartialEq, Eq)]
1111pub enum LateRaise {
1112 NoConfig,
1114 AlreadyUp,
1116 Raise,
1118 Repoint { from: String, to: String },
1121}
1122
1123pub fn late_raise(running: Option<&WgConfig>, wanted: Option<&WgConfig>, interface_carries_address: bool) -> LateRaise {
1125 if let Some(running) = running {
1126 return match wanted {
1127 None => LateRaise::AlreadyUp,
1129 Some(w) if w.peers == running.peers && w.address == running.address && w.prefix == running.prefix && w.private_key == running.private_key => {
1130 LateRaise::AlreadyUp
1131 }
1132 Some(w) => LateRaise::Repoint { from: peers_said(running), to: peers_said(w) },
1133 };
1134 }
1135 match wanted {
1136 None => LateRaise::NoConfig,
1137 Some(_) if interface_carries_address => LateRaise::AlreadyUp,
1138 Some(_) => LateRaise::Raise,
1139 }
1140}
1141
1142pub fn raise_from_config(data_root: &Path) -> Result<bool> {
1149 let late = LATE_TUNNEL.lock().unwrap_or_else(|p| p.into_inner()).clone();
1150 let running = late.as_ref().and_then(Tunnel::running_config);
1151 let wanted = WgConfig::resolve(data_root)?;
1152 let carries = wanted
1153 .as_ref()
1154 .map(|c| appliance_pid1::net::interface_ipv4(&c.interface).ok().flatten() == Some(c.address))
1155 .unwrap_or(false);
1156 match late_raise(running.as_ref(), wanted.as_ref(), carries) {
1157 LateRaise::NoConfig => return Ok(false),
1158 LateRaise::AlreadyUp => return Ok(true),
1159 LateRaise::Repoint { from, to } => {
1160 let tunnel = late.expect("Repoint is planned only for a running tunnel");
1161 let wanted = wanted.expect("Repoint is planned only for a configuration");
1162 tunnel.repoint(&wanted).with_context(|| format!("re-pointing {} from [{from}] to [{to}]", wanted.interface))?;
1163 tracing::info!(
1164 interface = %wanted.interface,
1165 from = %from,
1166 to = %to,
1167 "WireGuard RE-POINTED in this process: the configuration names another peer (the front's relay \
1168 moved or was re-keyed — a front recharged onto new addresses); the old pump is stopped and \
1169 the new one dials it now, without a restart"
1170 );
1171 if let Ok(rt) = tokio::runtime::Handle::try_current() {
1172 rt.spawn(handshake_watch(tunnel, 20, std::time::Duration::from_secs(3)));
1173 }
1174 return Ok(true);
1175 }
1176 LateRaise::Raise => {}
1177 }
1178 let config = wanted.expect("Raise is planned only for a configuration");
1179 let tunnel = establish(&config)?;
1180 if let Ok(rt) = tokio::runtime::Handle::try_current() {
1181 rt.spawn(handshake_watch(tunnel.clone(), 20, std::time::Duration::from_secs(3)));
1182 }
1183 *LATE_TUNNEL.lock().unwrap_or_else(|p| p.into_inner()) = Some(tunnel);
1184 Ok(true)
1185}
1186
1187fn open_pump(config: &WgConfig) -> Result<TunnelHandle> {
1189 let tun = TunDevice::create(&config.interface, DEFAULT_MTU).with_context(|| {
1194 format!(
1195 "creating the tun device {}. This needs CAP_NET_ADMIN and /dev/net/tun \
1196 (CONFIG_TUN, builtin on the appliance kernel) — and nothing else: no wireguard \
1197 module, no wg(8)",
1198 config.interface
1199 )
1200 })?;
1201
1202 appliance_pid1::net::apply(&appliance_pid1::net::NetPlan {
1213 interface: config.interface.clone(),
1214 address: config.address,
1215 prefix: config.prefix,
1216 gateway: None,
1217 })
1218 .with_context(|| {
1219 format!(
1220 "putting {}/{} on {}",
1221 config.address, config.prefix, config.interface
1222 )
1223 })?;
1224
1225 let handle = dataplane::spawn(tun, &config.tunnel_spec()).with_context(|| {
1227 format!(
1228 "starting the userspace WireGuard pump on {}",
1229 config.interface
1230 )
1231 })?;
1232 Ok(handle)
1233}
1234
1235fn establish(config: &WgConfig) -> Result<Tunnel> {
1236 let handle = open_pump(config)?;
1237 for (addr, prefix) in config.unrouted_allowed_ips() {
1238 tracing::warn!(
1239 allowed_ip = %format!("{addr}/{prefix}"),
1240 interface = %config.interface,
1241 tunnel_subnet = %format!("{}/{}", config.address, config.prefix),
1242 "an AllowedIPs entry falls outside this interface's own subnet, so the kernel has \
1243 no route to it. gunnar installs no routes beyond the address's connected one; \
1244 traffic to that prefix will not enter the tunnel"
1245 );
1246 }
1247
1248 let tunnel = Tunnel {
1249 interface: config.interface.clone(),
1250 address: config.address,
1251 prefix: config.prefix,
1252 peers: config.peers.len(),
1253 public_key: config.public_key(),
1254 local_port: handle.local_port(),
1255 config: Arc::new(std::sync::Mutex::new(config.clone())),
1256 handle: Arc::new(std::sync::Mutex::new(Some(handle))),
1257 };
1258 tracing::info!(
1259 interface = %tunnel.interface,
1260 address = %tunnel.address,
1261 prefix = tunnel.prefix,
1262 public_key = base64_key(&tunnel.public_key),
1263 listen_port = tunnel.local_port,
1264 peers = tunnel.peers,
1265 mtu = DEFAULT_MTU,
1266 "WireGuard up in USERSPACE: boringtun compiled into this binary, no kernel module, no \
1267 wg(8), no wg-quick, no shell. This address is SEEDED state and does not move when the \
1268 DHCP lease does — but it is only reachable once a peer whose [Peer] stanza carries the \
1269 public key above completes a handshake"
1270 );
1271 crate::tunnel_addr::publish(Some(tunnel.address));
1273 Ok(tunnel)
1274}
1275
1276pub async fn handshake_watch(tunnel: Tunnel, attempts: usize, every: std::time::Duration) {
1288 for attempt in 1..=attempts {
1289 tokio::time::sleep(every).await;
1290 let peers = tunnel.peer_stats();
1291 if let Some(up) = peers.iter().find(|p| p.handshaken()) {
1292 tracing::info!(
1293 interface = %tunnel.interface,
1294 address = %tunnel.address,
1295 peer = %base64_key(&up.public_key),
1296 handshake_age_secs = up.last_handshake_ago.map(|d| d.as_secs()),
1297 rx_bytes = up.rx_bytes,
1298 tx_bytes = up.tx_bytes,
1299 endpoint = ?up.endpoint,
1300 "WireGuard HANDSHAKE COMPLETED: this appliance is reachable at {} \
1301 over the tunnel, and that address does not change when the DHCP \
1302 lease does",
1303 tunnel.address
1304 );
1305 return;
1306 }
1307 tracing::debug!(
1308 interface = %tunnel.interface,
1309 attempt,
1310 peers = peers.len(),
1311 "no WireGuard handshake yet"
1312 );
1313 }
1314 tracing::warn!(
1315 interface = %tunnel.interface,
1316 peers = tunnel.peers,
1317 "WireGuard is configured and NO peer has completed a handshake. The tunnel address {} \
1318 is on the interface but nothing is on the other end of it — check the peer's endpoint, \
1319 its AllowedIPs, and that UDP reaches this machine",
1320 tunnel.address
1321 );
1322}
1323
1324pub(crate) fn base64_key(key: &[u8; KEY_LEN]) -> String {
1326 keys::encode_key(key)
1327}
1328
1329fn banner_line(
1336 interface: &str,
1337 address: Ipv4Addr,
1338 prefix: u8,
1339 public_key: &[u8; KEY_LEN],
1340 peers: usize,
1341 local_port: u16,
1342) -> String {
1343 format!(
1344 "wg {address}/{prefix} on {interface} key {} ({peers} peer{}, udp {local_port})",
1345 base64_key(public_key),
1346 if peers == 1 { "" } else { "s" },
1347 )
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352
1353 #[test]
1360 fn a_refeed_naming_another_front_relay_repoints_the_running_tunnel() {
1361 let conf = |endpoint: &str, key: &str| {
1362 WgConfig::parse(
1363 "wg0",
1364 &format!(
1365 "[Interface]\nPrivateKey = {}\nAddress = 10.44.0.2/24\n\n[Peer]\nPublicKey = {key}\nAllowedIPs = 10.44.0.0/24\nEndpoint = {endpoint}\nPersistentKeepalive = 25\n",
1366 base64_key(&[7u8; KEY_LEN])
1367 ),
1368 )
1369 .unwrap()
1370 };
1371 let old = conf("127.86.33.51:51821", &base64_key(&[1u8; KEY_LEN]));
1372 let moved = conf("127.86.33.10:51821", &base64_key(&[1u8; KEY_LEN]));
1373 let rekeyed = conf("127.86.33.10:51821", &base64_key(&[2u8; KEY_LEN]));
1374 assert_eq!(late_raise(Some(&old), Some(&old), true), LateRaise::AlreadyUp, "the same front: nothing to do");
1375 assert!(matches!(late_raise(Some(&old), Some(&moved), true), LateRaise::Repoint { .. }), "the front moved: re-point, not 'already up'");
1376 assert!(matches!(late_raise(Some(&old), Some(&rekeyed), true), LateRaise::Repoint { .. }), "the relay's key changed: re-point");
1377 assert_eq!(late_raise(None, Some(&old), false), LateRaise::Raise);
1378 assert_eq!(late_raise(None, Some(&old), true), LateRaise::AlreadyUp);
1379 assert_eq!(late_raise(None, None, false), LateRaise::NoConfig);
1380 assert_eq!(late_raise(Some(&old), None, true), LateRaise::AlreadyUp, "a configuration that went away does not take the running tunnel down");
1381 }
1382
1383 use super::*;
1384
1385 fn paired_root() -> tempfile::TempDir {
1388 let dir = tempfile::tempdir().unwrap();
1389 let root = dir.path();
1390 std::fs::create_dir_all(root.join("pairing")).unwrap();
1391 std::fs::write(root.join(crate::pairing::SEED_REL), "# the medium's seed\n").unwrap();
1392 let private = crate::pairing::ensure_tunnel_key(root).unwrap();
1393 let conf = format!(
1394 "{}: test\n[Interface]\nPrivateKey = {private}\nAddress = 10.44.0.2/24\n\n[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\nAllowedIPs = 10.44.0.0/24\nEndpoint = 95.111.211.189:51820\nPersistentKeepalive = 25\n",
1395 crate::pairing::CONF_MARK
1396 );
1397 std::fs::write(root.join(CONFIG_FILE), conf).unwrap();
1398 dir
1399 }
1400
1401 #[test]
1405 fn a_paired_data_root_boots_the_config_lane_and_never_the_ceremony() {
1406 let paired = paired_root();
1407 assert_eq!(boot_lane(true, paired.path()), BootLane::Config);
1408 std::fs::remove_file(paired.path().join(crate::pairing::SEED_REL)).unwrap();
1411 assert_eq!(boot_lane(true, paired.path()), BootLane::Config);
1412 let seed_only = tempfile::tempdir().unwrap();
1413 std::fs::create_dir_all(seed_only.path().join("pairing")).unwrap();
1414 std::fs::write(seed_only.path().join(crate::pairing::SEED_REL), "x").unwrap();
1415 assert_eq!(boot_lane(true, seed_only.path()), BootLane::Config);
1416 let bare = tempfile::tempdir().unwrap();
1419 assert_eq!(boot_lane(true, bare.path()), BootLane::Ceremony);
1420 assert_eq!(boot_lane(false, bare.path()), BootLane::Config);
1421 std::fs::write(bare.path().join(CONFIG_FILE), "[Interface]\n").unwrap();
1422 assert_eq!(boot_lane(true, bare.path()), BootLane::Ceremony);
1423 }
1424
1425 #[test]
1428 fn booting_a_paired_root_with_the_gate_on_does_not_refuse_and_mints_no_ceremony_identity() {
1429 if rustix::process::geteuid().is_root() {
1430 return; }
1432 let paired = paired_root();
1433 let got = bring_up_with(true, paired.path());
1434 assert!(got.is_ok(), "a paired box must boot: {:?}", got.err());
1435 assert!(!crate::appliance::wg_dir(paired.path()).exists(), "the ceremony must stay out");
1436 }
1437
1438 #[test]
1441 fn tunnel_show_on_a_paired_box_reports_the_pairing_tunnel() {
1442 let paired = paired_root();
1443 let root = paired.path();
1444 let private = crate::pairing::ensure_tunnel_key(root).unwrap();
1445 let want = wg_appliance_core::public_from_private(&private).unwrap();
1446 let shown = crate::appliance::tunnel_address_answer(root, "", false).expect("show answers");
1447 assert_eq!(shown.wg_public_key, want);
1448 assert_eq!(shown.address, "10.44.0.2/24");
1449 assert!(!shown.changed);
1450 let refused = crate::appliance::tunnel_address_answer(root, "10.44.0.9/24", true).expect_err("refused");
1451 assert!(format!("{refused:?}").contains("tunnel-owned-by-pairing"), "{refused:?}");
1452 }
1453
1454 #[test]
1466 fn arming_both_lanes_is_refused_and_the_refusal_names_the_source() {
1467 let root = Path::new("/srv/gunnar-data");
1468
1469 assert_eq!(
1470 seeded_lane_conflict(root, false, false),
1471 None,
1472 "the ordinary ceremony boot — no seeded source — must NOT refuse"
1473 );
1474
1475 let by_file = seeded_lane_conflict(root, true, false).expect("wg0.conf conflicts");
1476 assert!(
1477 by_file.contains("/srv/gunnar-data/wg0.conf"),
1478 "name the file the operator has to remove: {by_file}"
1479 );
1480 assert!(
1481 by_file.contains("/srv/gunnar-data/wg"),
1482 "and the ceremony directory it would have minted into: {by_file}"
1483 );
1484 assert!(
1485 by_file.contains(crate::appliance::GATE_ENV),
1486 "and the gate to unset instead: {by_file}"
1487 );
1488
1489 let by_param = seeded_lane_conflict(root, false, true).expect("the boot param conflicts");
1490 assert!(
1491 by_param.contains(BOOT_PARAM),
1492 "name the boot parameter, not the absent file: {by_param}"
1493 );
1494 assert!(
1495 !by_param.contains("wg0.conf"),
1496 "a file that is not there must not be blamed: {by_param}"
1497 );
1498
1499 let both = seeded_lane_conflict(root, true, true).expect("both conflict");
1502 assert!(both.contains(BOOT_PARAM), "{both}");
1503 }
1504
1505 const GOOD: &str = "\
1507[Interface]
1508PrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=
1509Address = 10.99.0.2/24
1510ListenPort = 51820
1511
1512# the box on the other end
1513[Peer]
1514PublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=
1515AllowedIPs = 10.99.0.1/32, 10.99.0.0/24
1516Endpoint = 10.0.2.2:51820
1517PersistentKeepalive = 25
1518";
1519
1520 #[test]
1521 fn a_full_config_parses_into_every_field() {
1522 let cfg = WgConfig::parse("wg0", GOOD).expect("a legal config");
1523 assert_eq!(cfg.interface, "wg0");
1524 assert_eq!(cfg.address, Ipv4Addr::new(10, 99, 0, 2));
1525 assert_eq!(cfg.prefix, 24);
1526 assert_eq!(cfg.listen_port, Some(51820));
1527 assert_eq!(cfg.peers.len(), 1);
1528 let peer = &cfg.peers[0];
1529 assert_eq!(
1530 peer.allowed_ips,
1531 vec![
1532 (Ipv4Addr::new(10, 99, 0, 1), 32),
1533 (Ipv4Addr::new(10, 99, 0, 0), 24),
1534 ]
1535 );
1536 assert_eq!(peer.endpoint, Some("10.0.2.2:51820".parse().unwrap()));
1537 assert_eq!(peer.persistent_keepalive, Some(25));
1538 assert_eq!(peer.preshared_key, None);
1539 assert_eq!(cfg.private_key.len(), KEY_LEN);
1541 assert_ne!(cfg.private_key, [0; KEY_LEN]);
1542 }
1543
1544 #[test]
1546 fn malformed_configs_are_refused_rather_than_defaulted() {
1547 for (why, bad) in [
1548 ("no PrivateKey", "[Interface]\nAddress = 10.0.0.1/24\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\n"),
1549 ("no Address", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\n"),
1550 ("no peer at all", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\nAddress = 10.0.0.1/24\n"),
1551 ("a peer with no PublicKey", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\nAddress = 10.0.0.1/24\n[Peer]\nAllowedIPs = 10.0.0.2/32\n"),
1552 ("a peer with no AllowedIPs", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\nAddress = 10.0.0.1/24\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\n"),
1553 ("a key that is not 32 bytes", "[Interface]\nPrivateKey = c2hvcnQ=\nAddress = 10.0.0.1/24\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\n"),
1554 ("a key that is not base64", "[Interface]\nPrivateKey = ****\nAddress = 10.0.0.1/24\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\n"),
1555 ("a /33 address", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\nAddress = 10.0.0.1/33\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\n"),
1556 ("a hostname endpoint", "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\nAddress = 10.0.0.1/24\n[Peer]\nPublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\nAllowedIPs = 10.0.0.2/32\nEndpoint = peer.example.com:51820\n"),
1557 ("a key before any section", "PrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\n"),
1558 ("a line that is not Key = value", "[Interface]\nPrivateKey\n"),
1559 ("an unknown section", "[Tunnel]\nPrivateKey = x\n"),
1560 ("an unknown key", "[Interface]\nWibble = 3\n"),
1561 ] {
1562 assert!(
1563 WgConfig::parse("wg0", bad).is_err(),
1564 "WgConfig::parse accepted a config with {why}; a parser that accepts everything \
1565 configures nothing and reports success"
1566 );
1567 }
1568 }
1569
1570 #[test]
1575 fn wg_quick_keys_gunnar_does_not_implement_are_refused_by_name() {
1576 for key in [
1577 "DNS = 1.1.1.1",
1578 "MTU = 1380",
1579 "Table = off",
1580 "PostUp = /bin/true",
1581 ] {
1582 let text = format!(
1583 "[Interface]\nPrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\n\
1584 Address = 10.0.0.1/24\n{key}\n[Peer]\n\
1585 PublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\n\
1586 AllowedIPs = 10.0.0.2/32\n"
1587 );
1588 let err = WgConfig::parse("wg0", &text).expect_err("must be refused");
1589 let msg = format!("{err:#}");
1590 assert!(
1591 msg.contains("does NOT implement"),
1592 "{key} was refused for the wrong reason: {msg}"
1593 );
1594 }
1595 }
1596
1597 #[test]
1601 fn comments_padding_and_line_numbers_survive_the_shared_tokenizer() {
1602 let cfg = WgConfig::parse(
1603 "wg0",
1604 "# a leading comment\n\
1605 [Interface] ; and a trailing one\n\
1606 PrivateKey = gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\n\
1607 Address = 10.0.0.1/24\n\
1608 \n\
1609 [Peer]\n\
1610 PublicKey = aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=\n\
1611 AllowedIPs = 10.0.0.2/32\n\
1612 PersistentKeepalive = 25 # keep the NAT open\n",
1613 )
1614 .expect("comments and blank lines are not errors");
1615 assert_eq!(
1616 cfg.private_key,
1617 parse_key("gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=").unwrap(),
1618 "a base64 key's trailing `=` padding must survive the key/value split"
1619 );
1620 assert_eq!(cfg.peers[0].persistent_keepalive, Some(25));
1621
1622 let err = WgConfig::parse("wg0", "# one\n\n[Interface]\nWibble = 3\n").unwrap_err();
1625 assert!(format!("{err:#}").contains("wg0.conf:4:"), "{err:#}");
1626 }
1627
1628 #[test]
1629 fn allowed_ips_outside_the_tunnel_subnet_are_reported_as_unrouted() {
1630 let cfg = WgConfig::parse("wg0", GOOD).expect("a legal config");
1631 assert!(
1633 cfg.unrouted_allowed_ips().is_empty(),
1634 "the ordinary case must be silent, or the warning is noise: {:?}",
1635 cfg.unrouted_allowed_ips()
1636 );
1637 let wide = GOOD.replace(
1639 "AllowedIPs = 10.99.0.1/32, 10.99.0.0/24",
1640 "AllowedIPs = 192.168.7.0/24",
1641 );
1642 let cfg = WgConfig::parse("wg0", &wide).expect("a legal config");
1643 assert_eq!(
1644 cfg.unrouted_allowed_ips(),
1645 vec![(Ipv4Addr::new(192, 168, 7, 0), 24)]
1646 );
1647 }
1648
1649 const HUB_PUB: &str = "aD8IQp7Jf8KgM5RqJUP1NaPJu6SQ1EUkPEqk118XIRo=";
1652
1653 #[test]
1654 fn the_boot_param_carries_the_whole_non_secret_half() {
1655 let spec = format!(
1656 "addr=10.99.0.2/24,peer={HUB_PUB},endpoint=10.0.2.2:51820,\
1657 allowed=10.99.0.0/24,keepalive=25"
1658 );
1659 let cfg = WgConfig::parse_boot_param("wg0", &spec, [0x11; KEY_LEN]).expect("a legal spec");
1660 assert_eq!(cfg.address, Ipv4Addr::new(10, 99, 0, 2));
1661 assert_eq!(cfg.prefix, 24);
1662 assert_eq!(
1663 cfg.private_key, [0x11; KEY_LEN],
1664 "the key comes from the FILE, not the spec"
1665 );
1666 assert_eq!(
1667 cfg.listen_port, None,
1668 "a dial-out appliance needs no fixed port"
1669 );
1670 assert_eq!(cfg.peers.len(), 1);
1671 assert_eq!(cfg.peers[0].public_key, parse_key(HUB_PUB).unwrap());
1672 assert_eq!(
1673 cfg.peers[0].endpoint,
1674 Some("10.0.2.2:51820".parse().unwrap())
1675 );
1676 assert_eq!(cfg.peers[0].persistent_keepalive, Some(25));
1677 assert_eq!(
1678 cfg.peers[0].allowed_ips,
1679 vec![(Ipv4Addr::new(10, 99, 0, 0), 24)]
1680 );
1681 }
1682
1683 #[test]
1684 fn the_boot_param_needs_only_addr_peer_and_allowed() {
1685 let spec = format!("addr=10.99.0.2/24,peer={HUB_PUB},allowed=10.99.0.0/24");
1686 let cfg = WgConfig::parse_boot_param("wg0", &spec, [0x11; KEY_LEN]).expect("a legal spec");
1687 assert_eq!(
1688 cfg.peers[0].endpoint, None,
1689 "a hub that dials US needs no endpoint here"
1690 );
1691 assert_eq!(cfg.peers[0].persistent_keepalive, None);
1692 }
1693
1694 #[test]
1698 fn the_boot_param_refuses_garbage_and_refuses_to_carry_a_private_key() {
1699 let good = format!("addr=10.99.0.2/24,peer={HUB_PUB},allowed=10.99.0.0/24");
1700 for (why, bad) in [
1701 ("no addr", format!("peer={HUB_PUB},allowed=10.99.0.0/24")),
1702 (
1703 "no peer",
1704 "addr=10.99.0.2/24,allowed=10.99.0.0/24".to_string(),
1705 ),
1706 ("no allowed", format!("addr=10.99.0.2/24,peer={HUB_PUB}")),
1707 ("a field that is not key=value", format!("{good},nonsense")),
1708 (
1709 "a bad endpoint",
1710 format!("{good},endpoint=hub.example.com:51820"),
1711 ),
1712 (
1713 "a bad prefix",
1714 format!("addr=10.99.0.2/33,peer={HUB_PUB},allowed=10.0.0.0/8"),
1715 ),
1716 (
1717 "a peer key that is not 32 bytes",
1718 "addr=10.99.0.2/24,peer=c2hvcnQ=,allowed=10.0.0.0/8".to_string(),
1719 ),
1720 ] {
1721 assert!(
1722 WgConfig::parse_boot_param("wg0", &bad, [0x11; KEY_LEN]).is_err(),
1723 "the boot parameter accepted a spec with {why}"
1724 );
1725 }
1726 for secret in ["privatekey", "private", "key", "PrivateKey"] {
1729 let bad = format!("{good},{secret}=gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=");
1730 let err = WgConfig::parse_boot_param("wg0", &bad, [0x11; KEY_LEN])
1731 .expect_err("a private key on the cmdline must be refused");
1732 assert!(
1733 format!("{err:#}").contains("world-readable"),
1734 "the refusal must say WHY: {err:#}"
1735 );
1736 }
1737 }
1738
1739 #[test]
1748 fn a_file_and_a_boot_param_together_are_refused_rather_than_ranked() {
1749 let dir = tempfile::tempdir().expect("a scratch directory");
1750 let dir = dir.path();
1751 std::fs::write(dir.join(CONFIG_FILE), GOOD).expect("write a config");
1752 let spec = format!("addr=10.99.0.2/24,peer={HUB_PUB},allowed=10.99.0.0/24");
1753 let err = WgConfig::resolve_with(dir, Some(&spec))
1754 .expect_err("two configurations must not be ranked");
1755 assert!(format!("{err:#}").contains("refuses to guess"), "{err:#}");
1756 }
1757
1758 #[test]
1762 fn the_boot_param_without_a_seeded_key_says_which_file_is_missing() {
1763 let dir = tempfile::tempdir().expect("a scratch directory");
1764 let dir = dir.path();
1765 let spec = format!("addr=10.99.0.2/24,peer={HUB_PUB},allowed=10.99.0.0/24");
1766 let err =
1767 WgConfig::resolve_with(dir, Some(&spec)).expect_err("no key file means no tunnel");
1768 assert!(
1769 format!("{err:#}").contains(KEY_FILE),
1770 "the error must name the file: {err:#}"
1771 );
1772 std::fs::write(
1774 dir.join(KEY_FILE),
1775 "gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=\n",
1776 )
1777 .expect("seed the key");
1778 let ok = WgConfig::resolve_with(dir, Some(&spec))
1779 .expect("a seeded key completes the boot-param form")
1780 .expect("a configuration");
1781 assert_eq!(
1782 ok.private_key,
1783 parse_key("gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=").unwrap()
1784 );
1785 assert_eq!(ok.address, Ipv4Addr::new(10, 99, 0, 2));
1786 }
1787
1788 #[test]
1795 fn neither_source_is_none_and_the_file_alone_resolves() {
1796 let dir = tempfile::tempdir().expect("a scratch directory");
1797 let dir = dir.path();
1798 assert!(
1799 WgConfig::resolve_with(dir, None)
1800 .expect("no sources is not a failure")
1801 .is_none(),
1802 "an unconfigured data root must mean no tunnel, not a broken boot"
1803 );
1804 std::fs::write(dir.join(CONFIG_FILE), GOOD).expect("write a config");
1805 let cfg = WgConfig::resolve_with(dir, None)
1806 .expect("a legal config file")
1807 .expect("present");
1808 assert_eq!(cfg.interface, "wg0");
1809 assert_eq!(cfg.address, Ipv4Addr::new(10, 99, 0, 2));
1810 }
1811
1812 #[test]
1813 fn a_missing_config_file_is_none_and_not_an_error() {
1814 let dir = std::env::temp_dir().join(format!("gunnar-wg-none-{}", std::process::id()));
1815 std::fs::create_dir_all(&dir).expect("a scratch directory");
1816 assert!(
1817 WgConfig::from_data_root(&dir)
1818 .expect("absence is not a failure")
1819 .is_none(),
1820 "no wg0.conf must mean no tunnel, not a broken boot"
1821 );
1822 std::fs::write(dir.join(CONFIG_FILE), GOOD).expect("write a config");
1823 let cfg = WgConfig::from_data_root(&dir)
1824 .expect("a legal config")
1825 .expect("present");
1826 assert_eq!(
1827 cfg.interface, "wg0",
1828 "the interface name comes from the file's stem"
1829 );
1830 std::fs::remove_dir_all(&dir).ok();
1831 }
1832
1833 #[test]
1843 fn the_banner_prints_the_public_key_wg_pubkey_derives() {
1844 const ITS_PUBLIC: &str = "R/1RsV4yjvIeEUApgRu3u584R5/WCKgKaPqHieam4TU=";
1845 let cfg = WgConfig::parse("wg0", GOOD).expect("a legal config");
1846 assert_eq!(
1847 base64_key(&cfg.public_key()),
1848 ITS_PUBLIC,
1849 "the identity gunnar prints is not the one wg derives from the same private key"
1850 );
1851 let other = GOOD.replace(
1854 "gEi6mvh6b5rKDg7UkleGpsJLlA9ueOKyWhjqJqFjWHg=",
1855 "cGVlcktleVRoYXRJc0RpZmZlcmVudEZyb21UaGVPbjA=",
1856 );
1857 let other = WgConfig::parse("wg0", &other).expect("a legal config");
1858 assert_ne!(other.public_key(), cfg.public_key());
1859 }
1860
1861 #[test]
1866 fn the_banner_line_carries_address_key_port_and_peer_count() {
1867 let cfg = WgConfig::parse("wg0", GOOD).expect("a legal config");
1868 let line = banner_line("wg0", cfg.address, cfg.prefix, &cfg.public_key(), 1, 51820);
1869 assert!(line.contains("10.99.0.2/24"), "{line}");
1870 assert!(line.contains("on wg0"), "{line}");
1871 assert!(
1872 line.contains("R/1RsV4yjvIeEUApgRu3u584R5/WCKgKaPqHieam4TU="),
1873 "the banner must print the identity a hub allowlists: {line}"
1874 );
1875 assert!(line.contains("(1 peer,"), "singular: {line}");
1876 assert!(line.contains("udp 51820"), "{line}");
1877 let two = banner_line("wg0", cfg.address, cfg.prefix, &cfg.public_key(), 2, 40000);
1878 assert!(two.contains("(2 peers,"), "plural: {two}");
1879 }
1880
1881 #[test]
1882 fn base64_round_trips_the_way_wg_prints_it() {
1883 let key = [0x22u8; KEY_LEN];
1884 let text = base64_key(&key);
1885 assert_eq!(
1886 text.len(),
1887 44,
1888 "a 32-byte key is 44 padded base64 characters"
1889 );
1890 assert_eq!(parse_key(&text).expect("its own output"), key);
1891 }
1892
1893 #[test]
1897 fn the_data_plane_spec_carries_every_configured_field() {
1898 let cfg = WgConfig::parse("wg0", GOOD).expect("a legal config");
1899 let spec = cfg.tunnel_spec();
1900 assert_eq!(spec.private_key, cfg.private_key);
1901 assert_eq!(spec.listen_port, Some(51820));
1902 assert_eq!(spec.peers.len(), 1);
1903 assert_eq!(spec.peers[0].public_key, cfg.peers[0].public_key);
1904 assert_eq!(
1905 spec.peers[0].endpoint,
1906 Some("10.0.2.2:51820".parse().unwrap())
1907 );
1908 assert_eq!(spec.peers[0].persistent_keepalive, Some(25));
1909 assert_eq!(
1910 spec.peers[0].allowed_ips,
1911 vec![
1912 (Ipv4Addr::new(10, 99, 0, 1), 32),
1913 (Ipv4Addr::new(10, 99, 0, 0), 24)
1914 ],
1915 "every AllowedIPs entry must reach the router, or the tunnel carries nothing"
1916 );
1917 let router = dataplane::Router::new(&spec.peers);
1919 assert_eq!(router.select("10.99.0.1".parse().unwrap()), Some(0));
1920 assert_eq!(router.select("10.99.0.77".parse().unwrap()), Some(0));
1921 assert_eq!(
1922 router.select("8.8.8.8".parse().unwrap()),
1923 None,
1924 "a config with no default route must not silently acquire one"
1925 );
1926 }
1927}