diff --git a/src/main.rs b/src/main.rs index 88336f4..df66c8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; use std::process::Command; use std::sync::Mutex; @@ -32,8 +32,6 @@ type Pubkey = String; #[derive(Deserialize)] struct Config { - /// The Wireguard interface name - interface: Pubkey, /// The port to use for gossip inside the Wireguard mesh (must be the same on all nodes) gossip_port: u16, /// The secret to use to authenticate nodes between them @@ -58,8 +56,12 @@ struct Config { struct Peer { /// The peer's Wireguard public key pubkey: Pubkey, - /// The peer's Wireguard address + /// The destination used for gossip packets address: IpAddr, + /// The Wireguard interface name + interface: String, + /// The endpoint port + port: u16, /// An optionnal Wireguard endpoint used to initialize a connection to this peer endpoint: Option, } @@ -115,9 +117,15 @@ fn kdf(secret: &str) -> xsalsa20poly1305::Key { hash.as_bytes().clone().into() } -fn wg_dump(config: &Config) -> Result<(Pubkey, u16, Vec<(Pubkey, Option, u64)>)> { +struct IfInfo { + our_pubkey: Pubkey, + listen_port: u16, + peers: Vec<(Pubkey, Option, u64)>, +} + +fn wg_dump(interface: &str) -> Result { let output = Command::new("wg") - .args(["show", &config.interface, "dump"]) + .args(["show", interface, "dump"]) .output()?; let mut lines = std::str::from_utf8(&output.stdout)?.split('\n'); @@ -125,7 +133,7 @@ fn wg_dump(config: &Config) -> Result<(Pubkey, u16, Vec<(Pubkey, Option Result<(Pubkey, u16, Vec<(Pubkey, Option>(); - Ok((our_pubkey, listen_port, peers)) + Ok(IfInfo { our_pubkey, listen_port, peers }) } // ============ DAEMON CODE ================= @@ -154,8 +162,6 @@ fn wg_dump(config: &Config) -> Result<(Pubkey, u16, Vec<(Pubkey, Option, } @@ -165,10 +171,10 @@ struct PeerInfo { gossip_ip: IpAddr, gossip_prio: u64, // Info retrieved from wireguard - endpoint: Option, + endpoint: Option, last_seen: u64, // Info received by LAN broadcast - lan_endpoint: Option<(SocketAddr, u64)>, + lan_endpoint: Option<(IpAddr, u64)>, } #[derive(Serialize, Deserialize, Debug)] @@ -177,12 +183,11 @@ enum Gossip { Pong, Announce { pubkey: Pubkey, - endpoints: Vec<(SocketAddr, u64)>, + endpoints: Vec<(IpAddr, u64)>, }, Request, LanBroadcast { pubkey: Pubkey, - listen_port: u16, }, } @@ -190,18 +195,31 @@ impl Daemon { fn new(config: Config) -> Result { let gossip_key = kdf(config.gossip_secret.as_deref().unwrap_or_default()); - let (our_pubkey, listen_port, _peers) = wg_dump(&config)?; + let interface_names = config.peers.iter().map(|peer| peer.interface.clone()).collect::>(); + let interfaces = interface_names.into_iter().map(|interface_name| wg_dump(&interface_name).map(|ifinfo| (interface_name, ifinfo))).collect::>>()?; let socket = UdpSocket::bind(SocketAddr::new("0.0.0.0".parse()?, config.gossip_port))?; socket.set_broadcast(true)?; + let peers = config.peers.iter().map(|peer_cfg| { + ( + peer_cfg.pubkey.clone(), + PeerInfo { + gossip_ip: peer_cfg.address, + gossip_prio: fasthash(format!("{}-{}", interfaces.iter().next().unwrap().1.our_pubkey, peer_cfg.pubkey).as_bytes()), + endpoint: None, // Is resolved as DNS name later + last_seen: u64::MAX, + lan_endpoint: None, + } + ) + }).collect(); + Ok(Daemon { config, gossip_key, - our_pubkey, - listen_port, socket, state: Mutex::new(State { - peers: HashMap::new(), + interfaces, + peers, gossip: HashMap::new(), }), }) @@ -236,7 +254,7 @@ impl Daemon { fn initialize(&self) -> Result<()> { let mut state = self.state.lock().unwrap(); - state.read_wg_peers(self)?; + state.read_wg_peers()?; state.setup_wg_peers(self, 0)?; Ok(()) } @@ -267,7 +285,7 @@ impl Daemon { let mut state = self.state.lock().unwrap(); // 1. Update local peers info of peers - state.read_wg_peers(self)?; + state.read_wg_peers()?; // 2. Send gossip for peers where there is a big update let announces = state @@ -322,12 +340,10 @@ impl Daemon { } Gossip::LanBroadcast { pubkey, - listen_port, } => { if self.config.lan_discovery { if let Some(peer) = state.peers.get_mut(&pubkey) { - let addr = SocketAddr::new(from.ip(), listen_port); - peer.lan_endpoint = Some((addr, time())); + peer.lan_endpoint = Some((from.ip(), time())); } } } @@ -393,8 +409,7 @@ impl Daemon { fn lan_broadcast_iter(&self) -> Result<()> { let packet = self.make_packet(&Gossip::LanBroadcast { - pubkey: self.our_pubkey.clone(), - listen_port: self.listen_port, + pubkey: self.our_pubkey(), })?; let addr = SocketAddr::new("255.255.255.255".parse().unwrap(), self.config.gossip_port); self.socket.send_to(&packet, addr)?; @@ -438,13 +453,16 @@ impl Daemon { gateway.addr, private_ip ); - gateway.add_port( - igd::PortMappingProtocol::UDP, - external_port, - SocketAddrV4::new(private_ip, self.listen_port), - IGD_LEASE_DURATION.as_secs() as u32, - "Wireguard via wgautomesh", - )?; + let ports = self.state.lock().unwrap().interfaces.iter().map(|(_, IfInfo { listen_port, .. })| *listen_port).collect::>(); + for listen_port in ports { + gateway.add_port( + igd::PortMappingProtocol::UDP, + external_port, + SocketAddrV4::new(private_ip, listen_port), + IGD_LEASE_DURATION.as_secs() as u32, + "Wireguard via wgautomesh", + )?; + } Ok(()) } @@ -465,11 +483,16 @@ impl Daemon { Ok([&nonce[..], &ciphertext[..]].concat()) } + + fn our_pubkey(&self) -> String { + self.state.lock().unwrap().interfaces.iter().next().unwrap().1.our_pubkey.clone() + } } struct State { peers: HashMap, - gossip: HashMap>, + gossip: HashMap>, + interfaces: HashMap, } impl State { @@ -499,7 +522,7 @@ impl State { &mut self, daemon: &Daemon, pubkey: Pubkey, - mut endpoints: Vec<(SocketAddr, u64)>, + mut endpoints: Vec<(IpAddr, u64)>, ) -> Result<()> { let propagate = { match self.gossip.get_mut(&pubkey) { @@ -541,36 +564,21 @@ impl State { Ok(()) } - fn read_wg_peers(&mut self, daemon: &Daemon) -> Result<()> { - let (_, _, wg_peers) = wg_dump(&daemon.config)?; + fn read_wg_peers(&mut self) -> Result<()> { // Clear old known endpoints if any for (_, peer) in self.peers.iter_mut() { peer.endpoint = None; } - for (pk, endpoint, last_seen) in wg_peers { - match self.peers.get_mut(&pk) { - Some(i) => { - i.endpoint = endpoint; - i.last_seen = last_seen; - } - None => { - let gossip_ip = match daemon.config.peers.iter().find(|x| x.pubkey == pk) { - Some(x) => x.address, - None => continue, - }; - let gossip_prio = fasthash(format!("{}-{}", daemon.our_pubkey, pk).as_bytes()); - self.peers.insert( - pk, - PeerInfo { - endpoint, - lan_endpoint: None, - gossip_prio, - gossip_ip, - last_seen, - }, - ); + for (ifname, ifinfo) in &mut self.interfaces { + *ifinfo = wg_dump(&ifname)?; + for (pk, endpoint, last_seen) in &mut ifinfo.peers { + if let Some(i) = self.peers.get_mut(&*pk) { + i.endpoint = endpoint.as_ref().map(SocketAddr::ip); + i.last_seen = *last_seen; + } else { + warn!("unknown peer: {}", pk); } } } @@ -582,7 +590,7 @@ impl State { let now = time(); for peer_cfg in daemon.config.peers.iter() { // Skip ourself - if peer_cfg.pubkey == daemon.our_pubkey { + if peer_cfg.pubkey == daemon.our_pubkey() { continue; } @@ -605,7 +613,7 @@ impl State { Command::new("wg") .args([ "set", - &daemon.config.interface, + &peer_cfg.interface, "peer", &peer_cfg.pubkey, "persistent-keepalive", @@ -631,11 +639,11 @@ impl State { .cloned() .unwrap_or_default(); if let Some(endpoint) = &peer_cfg.endpoint { - match endpoint.to_socket_addrs() { + match format!("{}:0", endpoint).to_socket_addrs() { Err(e) => error!("Could not resolve DNS for {}: {}", endpoint, e), Ok(iter) => { for addr in iter { - endpoints.push((addr, 0)); + endpoints.push((addr.ip(), 0)); } } } @@ -662,15 +670,15 @@ impl State { Command::new("wg") .args([ "set", - &daemon.config.interface, + &peer_cfg.interface, "peer", &peer_cfg.pubkey, "endpoint", - &endpoint.to_string(), + &SocketAddr::new(endpoint, peer_cfg.port).to_string(), "persistent-keepalive", "10", "allowed-ips", - &format!("{}/32", peer_cfg.address), + "::/0,0.0.0.0/0" ]) .output()?; let packet = daemon.make_packet(&Gossip::Ping)?; @@ -683,11 +691,11 @@ impl State { Command::new("wg") .args([ "set", - &daemon.config.interface, + &peer_cfg.interface, "peer", &peer_cfg.pubkey, "allowed-ips", - &format!("{}/32", peer_cfg.address), + "::/0,0.0.0.0/0" ]) .output()?; }