From 3cfa59b682ab1d3086a261fc6e16ee2b8018ebd8 Mon Sep 17 00:00:00 2001 From: Lyn Date: Tue, 14 Jan 2025 15:07:07 +0100 Subject: [PATCH] made IGD implementation compatible with IPv6 implementation, updated documentation for config structure changes --- README.md | 45 ++++++++++++----- config.toml | 3 +- src/igd.rs | 96 ++++++++++++++++++++---------------- src/main.rs | 138 ++++++++++++++++++++++++++++++++++++---------------- 4 files changed, 186 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 5210e91..ee45db0 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,6 @@ and closely mirror the structure of the configuration file described below. ### Sample configuration file ```toml -# The Wireguard interface to control. -interface = "wg0" - # The port wgautomesh will use to communicate from node to node. Wgautomesh # gossip communications occur inside the wireguard mesh network. gossip_port = 1666 @@ -42,9 +39,9 @@ gossip_port = 1666 # Enable discovery of other wgautomesh nodes on the same LAN using UDP broadcast. lan_discovery = true -# Enables UPnP/IGD forwarding of an external port to the Wireguard listening port -# on this node, for compatible routers/gateways. -upnp_forward_external_port = 33723 +# Enables UPnP/IGD forwarding of an external port to the Wireguard listening ports +# for compatible routers/gateways. For IPv4 port forwards, an external port needs to be specified in the interfaces section. +upnp_open_ports = true # The path to a file that contains the encryption secret wgautomesh uses to # communicate. This secret can be any arbitrary utf-8 string. The following @@ -58,27 +55,49 @@ gossip_secret_file = "/var/lib/wgautomesh/gossip_secret" # `[[peers]]` section when trying to establish connectivity. persist_file = "/var/lib/wgautomesh/state" -[[peers]] -pubkey = "7Nm7pMmyS7Nts1MB+loyD8u84ODxHPTkDu+uqQR6yDk=" -address = "10.14.1.2" -endpoint = "77.207.15.215:33722" +# Configuration for a wireguard interface +[[interfaces]] +# Interface name +name = "wg0" +# External port to forward to this interface via IGD. +# (Optional, if not used there won't be any IPv4 port forwards done for this interface). +upnp_ext_port_v4 = 51820 [[peers]] +# The Wireguard interface to use to connect with this peer. +interface = "wg0" +# Pubkey of the other peer +pubkey = "7Nm7pMmyS7Nts1MB+loyD8u84ODxHPTkDu+uqQR6yDk=" +# Mesh-internal wireguard address +address = "10.14.1.2" +# (Optional) endpoint address of this peer +endpoint = "77.207.15.215" +# (Optional) endpoint port +port = 33722 + +[[peers]] +interface = "wg0" pubkey = "lABn/axzD1jkFulX8c+K3B3CbKXORlIMDDoe8sQVxhs=" address = "10.14.1.3" -endpoint = "77.207.15.215:33723" +endpoint = "77.207.15.215" +port = 33723 [[peers]] +interface = "wg0" pubkey = "XLOYoMXF+PO4jcgfSVAk+thh4VmWx0wzWnb0xs08G1s=" address = "10.14.4.1" -endpoint = "bitfrost.fiber.shirokumo.net:33734" +endpoint = "bitfrost.fiber.shirokumo.net" +port = 33734 [[peers]] +interface = "wg0" pubkey = "smBQYUS60JDkNoqkTT7TgbpqFiM43005fcrT6472llI=" address = "10.14.2.33" -endpoint = "82.64.238.84:33733" +endpoint = "82.64.238.84" +port = 33733 [[peers]] +interface = "wg0" pubkey = "m9rLf+233X1VColmeVrM/xfDGro5W6Gk5N0zqcf32WY=" address = "10.14.3.1" ``` diff --git a/config.toml b/config.toml index 568d364..0d047a2 100644 --- a/config.toml +++ b/config.toml @@ -1,2 +1,3 @@ -interface = "route48" gossip_port = 1134 +[[interfaces]] +name = "wg0" diff --git a/src/igd.rs b/src/igd.rs index 6139efb..e62b95b 100644 --- a/src/igd.rs +++ b/src/igd.rs @@ -1,5 +1,6 @@ +use std::collections::HashMap; use log::*; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use futures::stream::{StreamExt}; use rupnp::{Device, Service}; use rupnp::ssdp::{SearchTarget, URN}; @@ -9,47 +10,52 @@ use anyhow::{anyhow, bail, Context, Result, Error}; const IGD_LEASE_DURATION: Duration = Duration::from_secs(300); const WAN_IPV6_FIREWALL_CONTROL: URN = URN::service("schemas-upnp-org", "WANIPv6FirewallControl", 1); const WAN_IP_CONNECTION: URN = URN::service("schemas-upnp-org", "WANIPConnection", 1); -pub async fn igd_loop_iter(listen_port:u16, external_port: u16, use_ipv6: bool) -> Result<()> { +pub async fn igd_loop_iter(portmap_v4:HashMap, portlist_v6: Vec) -> Result<()> { let lease_duration: u64 = IGD_LEASE_DURATION.as_secs(); //find gateway compatible with publishing the port for required IP version - let gateway = find_gateway(use_ipv6).await?; + let gateway = find_gateway(portlist_v6.len() >0).await?; let gateway_ip: IpAddr = IpAddr::from_str(gateway.url().host().unwrap())?; //find corresponding interface local IP to forward in gateway - let local_ip = select_local_ip_for_gateway(gateway_ip,use_ipv6)?; - println!("local_ip: {}", local_ip); + let (local_ipv4, local_ipv6) = select_local_ip_for_gateway(gateway_ip)?; + debug!( "Found gateway: {:?} at IP {}, making announce", gateway.friendly_name(), gateway.url().host().unwrap() ); - - if use_ipv6 { - create_ipv6_firewall_pinhole( - gateway, - &local_ip, - listen_port, - external_port, - &lease_duration, - ) - .await - } else { - create_ipv4_port_mapping( - gateway, - &local_ip, - listen_port, - external_port, - &lease_duration, - ) - .await + if local_ipv6.is_some() { + for listen_port in &portlist_v6 { + create_ipv6_firewall_pinhole( + &gateway, + &IpAddr::V6(local_ipv6.unwrap()), + listen_port, + &lease_duration, + ) + .await? + } } + if local_ipv4.is_some() { + for (listen_port, external_port) in &portmap_v4 { + create_ipv4_port_mapping( + &gateway, + &IpAddr::V4(local_ipv4.unwrap()), + listen_port, + external_port, + &lease_duration, + ) + .await? + } + } + Ok(()) + } /// Create a port mapping to forward a given Port for a given internal IPv4 async fn create_ipv4_port_mapping( - gateway: Device, + gateway: &Device, internal_ip: &IpAddr, - listen_port: u16, - external_port: u16, + listen_port: &u16, + external_port: &u16, lease_duration: &u64, ) -> Result<()> { let wan_ip_con_service = gateway @@ -83,10 +89,9 @@ async fn create_ipv4_port_mapping( } } /// Create a pinhole for a given IPv6 address on a given port -async fn create_ipv6_firewall_pinhole(gateway: Device, +async fn create_ipv6_firewall_pinhole(gateway: &Device, ip: &IpAddr, - listen_port: u16, - external_port: u16, + listen_port: &u16, lease_duration: &u64, ) -> Result<()> { let wan_ip6_fw_con = gateway @@ -102,15 +107,15 @@ async fn create_ipv6_firewall_pinhole(gateway: Device, } let arguments = format!( " -{external_port} + 17 {listen_port} {ip} {lease_duration}" ); debug!( - "Opening firewall pinhole for IP {} on internal port {}, external port {}", - ip, listen_port, external_port, + "Opening firewall pinhole for IP {} on port {}", + ip, listen_port, ); let result = wan_ip6_fw_con .action(gateway.url(), "AddPinhole", &arguments) @@ -188,18 +193,27 @@ fn interface_ips_in_same_subnet_as(ip_to_match: IpAddr) -> Result, E } /// Selects the local IP we tell the Gateway to port forward to (/pinhole) later on /// Note: As soon as this[https://github.com/jakobhellermann/ssdp-client/issues/11] is fixed and the dependency is upgraded in rupnp, we can simplify it -fn select_local_ip_for_gateway(gateway: IpAddr, output_ipv6: bool) -> Result { +fn select_local_ip_for_gateway(gateway: IpAddr) -> Result<(Option,Option), Error> { //get IPs in same subnet as Gateway let ips = interface_ips_in_same_subnet_as(gateway)?; //removes IPv6 that are not globally routable as pinholing those would be pointless let v6_cleaned_up = ips .iter() .filter(|ip| ip.is_ipv4() || (ip.is_global() && ip.is_ipv6())); - //filters IPs to match the ipv6 output criteria - let mut viable_ips = v6_cleaned_up.filter(|ip| ip.is_ipv6() == output_ipv6); - //return the first IP - viable_ips - .next() - .copied() - .ok_or_else(|| anyhow!("Couldn't find an IP address")) + //get the first IPv4 and IPv6 found + let mut first_ipv4 :Option = None; + let mut first_ipv6 :Option = None; + for ip in v6_cleaned_up { + match ip { + IpAddr::V4(ipv4) => { if first_ipv4.is_none() { first_ipv4 = Some(ipv4.clone()); } } + IpAddr::V6(ipv6) => { if first_ipv6.is_none() { first_ipv6 = Some(ipv6.clone()); } } + } + if first_ipv4.is_some() && first_ipv6.is_some() { + break; + } + } + if first_ipv4.is_none() && first_ipv6.is_none() { + bail!("Couldn't find any IP address") + } + Ok((first_ipv4, first_ipv6)) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 96533c7..a2fe1cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod igd; use igd::*; use std::collections::{HashMap, HashSet}; -use std::net::{IpAddr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket}; use std::process::Command; use std::sync::Mutex; use std::thread; @@ -45,12 +45,17 @@ struct Config { #[serde(default)] lan_discovery: bool, - /// Forward an external port to Wiregard using UPnP IGD - upnp_forward_external_port: Option, + /// Make the interfaces ports availible via IGD. + #[serde(default)] + upnp_open_ports: bool, /// The list of peers we try to connect to #[serde(default)] peers: Vec, + + /// Settings for the Wireguard interfaces (currently only necessary if you want to use igd features) + #[serde(default)] + interfaces: Vec, } #[derive(Deserialize)] @@ -59,13 +64,21 @@ struct Peer { pubkey: Pubkey, /// The destination used for gossip packets address: IpAddr, - /// The Wireguard interface name + /// The Wireguard interface name used to communicate with this peer interface: String, /// The endpoint port - port: u16, - /// An optionnal Wireguard endpoint used to initialize a connection to this peer + port: Option, + /// An optional Wireguard endpoint used to initialize a connection to this peer endpoint: Option, } +/// Settings for Wireguard interfaces +#[derive(Deserialize)] +struct InterfaceSetting { + /// The Wireguard interface name + name: String, + /// Specify the external port that should be IGD forwarded to this Wireguard interface. Only used for IPv4. + upnp_ext_port_v4: Option, +} fn main() -> Result<()> { pretty_env_logger::init(); @@ -200,7 +213,7 @@ impl Daemon { 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("::".parse()?, config.gossip_port))?; - //socket.set_broadcast(true)?; + socket.set_broadcast(true)?; socket.set_ttl(1)?; let our_pubkey = interfaces.iter().next().unwrap().1.our_pubkey.clone(); @@ -283,7 +296,7 @@ impl Daemon { error!("Wireguard configuration loop error: {}", e); } i = i + 1; - std::thread::sleep(TRY_INTERVAL); + thread::sleep(TRY_INTERVAL); } } @@ -323,7 +336,7 @@ impl Daemon { loop { if let Err(e) = self.recv_loop_iter() { error!("Receive loop error: {}", e); - std::thread::sleep(Duration::from_secs(10)); + thread::sleep(Duration::from_secs(10)); } } } @@ -391,7 +404,7 @@ impl Daemon { if let Err(e) = self.persist_state(file) { error!("Could not write persistent state to disk: {}", e); } - std::thread::sleep(PERSIST_INTERVAL); + thread::sleep(PERSIST_INTERVAL); } } } @@ -408,7 +421,7 @@ impl Daemon { if let Err(e) = self.lan_broadcast_iter() { error!("LAN broadcast loop error: {}", e); } - std::thread::sleep(LAN_BROADCAST_INTERVAL); + thread::sleep(LAN_BROADCAST_INTERVAL); } } } @@ -417,28 +430,70 @@ impl Daemon { let packet = self.make_packet(&Gossip::LanBroadcast { pubkey: self.our_pubkey.clone(), })?; - let addr = if self.config.ipv6 { - SocketAddr::new("ff05::1".parse().unwrap(), self.config.gossip_port) - } else { - SocketAddr::new("255.255.255.255".parse().unwrap(), self.config.gossip_port) - }; - self.socket.send_to(&packet, addr)?; + let broadcast_addr = SocketAddr::new("255.255.255.255".parse()?, self.config.gossip_port); + let broadcast_addrv6 = SocketAddr::new("ff05::1".parse()?, self.config.gossip_port); + self.socket.send_to(&packet, broadcast_addr)?; + self.socket.send_to(&packet, broadcast_addrv6)?; Ok(()) } fn igd_loop(&self) { - if let Some(external_port) = self.config.upnp_forward_external_port { + if self.config.upnp_open_ports { let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); rt.block_on(async { loop { - if let Err(e) = igd_loop_iter(self.listen_port,external_port, self.config.ipv6).await { - error!("IGD loop error: {}", e); + let (portmap_v4, portmap_v6) = self.generate_portmaps(); + if !portmap_v4.is_empty() || !portmap_v6.is_empty() { + if let Err(e) = igd_loop_iter(portmap_v4, portmap_v6).await { + error!("IGD loop error: {}", e); + } } tokio::time::sleep(IGD_INTERVAL).await; } }); } } + fn generate_portmaps(&self) -> (HashMap, Vec) { + let mut portmap_v4: HashMap = HashMap::new(); + //collect interfaces that have peers with IPv4 addresses (-> IPv4 port mappings need to exist) + let binding = self.state.lock().unwrap(); + let interfaces = binding.interfaces.iter(); + let v4ifs: Vec<_> = binding.interfaces.iter() + .filter_map(|(ifname, ifinfo)| { + if ifinfo.listen_port != 0 + && self.config.peers.iter().any(|peer| peer.interface == **ifname && peer.address.is_ipv4()) + { + Some((ifname, ifinfo)) + } else { + None + } + }) + .collect(); + //create portmap + for ifsetting in &self.config.interfaces { + if let Some(external_port) = ifsetting.upnp_ext_port_v4 { + if let Some((_ifname, ifinfo)) = v4ifs + .iter() + .find(|(ifname, _)| **ifname == ifsetting.name) + { + portmap_v4.insert(ifinfo.listen_port, external_port); + } + } + } + //collect ports of interfaces that have peers with IPv6 addresses (-> pinholes for the IPv6 listen ports should be created) + let v6ports: Vec = interfaces + .filter(|(ifname, ifinfo)| { + ifinfo.listen_port!=0 + && self.config + .peers + .iter() + .any(|peer| peer.interface == **ifname && peer.address.is_ipv6()) + }) + .map(|(_, ifinfo)|ifinfo.listen_port) + .collect(); + (portmap_v4, v6ports) + + } fn make_packet(&self, gossip: &Gossip) -> Result> { use xsalsa20poly1305::{ @@ -634,27 +689,28 @@ impl State { // Skip if we are already using that endpoint continue; } - - info!("Configure {} with endpoint {}", peer_cfg.pubkey, endpoint); - Command::new("wg") - .args([ - "set", - &peer_cfg.interface, - "peer", - &peer_cfg.pubkey, - "endpoint", - &SocketAddr::new(endpoint, peer_cfg.port).to_string(), - "persistent-keepalive", - "10", - "allowed-ips", - "::/0,0.0.0.0/0" - ]) - .output()?; - let packet = daemon.make_packet(&Gossip::Ping)?; - daemon.socket.send_to( - &packet, - SocketAddr::new(peer_cfg.address, daemon.config.gossip_port), - )?; + if let Some(port) = peer_cfg.port { + info!("Configure {} with endpoint {}", peer_cfg.pubkey, endpoint); + Command::new("wg") + .args([ + "set", + &peer_cfg.interface, + "peer", + &peer_cfg.pubkey, + "endpoint", + &SocketAddr::new(endpoint, port).to_string(), + "persistent-keepalive", + "10", + "allowed-ips", + "::/0,0.0.0.0/0" + ]) + .output()?; + let packet = daemon.make_packet(&Gossip::Ping)?; + daemon.socket.send_to( + &packet, + SocketAddr::new(peer_cfg.address, daemon.config.gossip_port), + )?; + } } else { info!("Configure {} with no known endpoint", peer_cfg.pubkey); Command::new("wg")