Compare commits

...

9 commits

Author SHA1 Message Date
Lyn
9482910911 update Cargo.lock 2025-01-15 18:14:15 +01:00
Lyn
845d6c9fc3 Merge remote-tracking branch 'yuka/main'
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/main.rs
2025-01-15 18:13:42 +01:00
Lyn
3cfa59b682 made IGD implementation compatible with IPv6 implementation, updated documentation for config structure changes 2025-01-15 17:31:48 +01:00
Lyn
d5cc055d39 int overflow no. 2 fixed 2025-01-14 13:55:22 +01:00
Lyn
26ed900d41 integer overflow fix 2025-01-13 16:44:47 +01:00
Lyn
a46acbb85c Merge remote-tracking branch 'yuka/main'
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/main.rs
2025-01-11 13:41:56 +01:00
Lyn
75b34fc48a added IPv6 support; rewrote the igd implementation 2025-01-11 12:33:36 +01:00
59d315b853 Merge pull request 'update cargo dependencies' (#7) from yuka/wgautomesh:bump-cargo-deps into main
Reviewed-on: Deuxfleurs/wgautomesh#7
2024-05-24 16:57:15 +00:00
Yureka
1fa9bcc3e9 update cargo dependencies 2024-05-22 11:58:43 +02:00
9 changed files with 2093 additions and 795 deletions

811
Cargo.lock generated

File diff suppressed because it is too large Load diff

1604
Cargo.nix

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "wgautomesh"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -11,12 +11,13 @@ anyhow = "1.0"
log = "0.4"
pretty_env_logger = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0.215", features = ["derive"] }
bincode = "1.3"
toml = { version = "0.8", default-features = false, features = ["parse"] }
xsalsa20poly1305 = "0.9"
blake3 = "1.5"
igd = { version = "0.12", default-features = false }
get_if_addrs = "0.5"
pnet = "0.35.0"
rupnp = "2.0.0"
tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros"] }
futures = "0.3.31"
ipnet = { version = "2.10.1", features = ["serde"] }

View file

@ -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"
```

View file

@ -1,2 +1,3 @@
interface = "route48"
gossip_port = 1134
[[interfaces]]
name = "wg0"

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

219
src/igd.rs Normal file
View file

@ -0,0 +1,219 @@
use std::collections::HashMap;
use log::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use futures::stream::{StreamExt};
use rupnp::{Device, Service};
use rupnp::ssdp::{SearchTarget, URN};
use std::str::FromStr;
use std::time::Duration;
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(portmap_v4:HashMap<u16,u16>, portlist_v6: Vec<u16>) -> 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(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_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 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,
internal_ip: &IpAddr,
listen_port: &u16,
external_port: &u16,
lease_duration: &u64,
) -> Result<()> {
let wan_ip_con_service = gateway
.find_service(&WAN_IP_CONNECTION)
.expect("Gateway passed doesn't offer the required service to create IPv4 port mapping");
let arguments = format!(
"<NewRemoteHost/>
<NewExternalPort>{external_port}</NewExternalPort>
<NewProtocol>UDP</NewProtocol>
<NewInternalPort>{listen_port}</NewInternalPort>
<NewInternalClient>{internal_ip}</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>Wireguard via wgautomesh</NewPortMappingDescription>
<NewLeaseDuration>{lease_duration}</NewLeaseDuration>"
);
debug!(
"Adding port mapping for internal IP {} on internal port {}, external port {}",
internal_ip, listen_port, external_port,
);
let result = wan_ip_con_service
.action(gateway.url(), "AddPortMapping", &arguments)
.await;
if result.is_ok() {
Ok(())
} else {
bail!(
"Error trying to add IPv4 port mapping: {}.\
Note: Have you checked whether your router allows this device to create (IPv4) port mappings?",
result.err().unwrap()
);
}
}
/// Create a pinhole for a given IPv6 address on a given port
async fn create_ipv6_firewall_pinhole(gateway: &Device,
ip: &IpAddr,
listen_port: &u16,
lease_duration: &u64,
) -> Result<()> {
let wan_ip6_fw_con = gateway
.find_service(&WAN_IPV6_FIREWALL_CONTROL)
.expect("Gateway passed doesn't offer the required service to create IPv6 pinholes");
let (firewall_enabled, can_create_inbound_pinhole) =
get_firewall_status(&gateway, &wan_ip6_fw_con).await;
if !firewall_enabled {
debug!("Gateway firewall is not enabled, incoming connections should be allowed as-is on all ports");
return Ok(());
} else if !can_create_inbound_pinhole {
bail!("Gateway said creating inbound IPv6 pinholes isn't allowed")
}
let arguments = format!(
"<RemoteHost/>
<RemotePort/>
<Protocol>17</Protocol>
<InternalPort>{listen_port}</InternalPort>
<InternalClient>{ip}</InternalClient>
<LeaseTime>{lease_duration}</LeaseTime>"
);
debug!(
"Opening firewall pinhole for IP {} on port {}",
ip, listen_port,
);
let result = wan_ip6_fw_con
.action(gateway.url(), "AddPinhole", &arguments)
.await;
if result.is_ok() {
Ok(())
} else {
bail!(
"Error trying to open IPv6 pinhole: {}\
Note: Have you checked whether your router allows this device to create (IPv6) pinholes?",
result.err().unwrap()
);
}
}
/// Asks the Gateway for the IPv6 Firewall status (whether the firewall is enabled AND whether devices in this network are allowed to create IPv6 pinholes per policy).
/// Note: This only works on IGDv2 supporting firewalls (-> any firewall that can do IPv6)
async fn get_firewall_status(gateway: &Device, igd_service: &Service) -> (bool, bool) {
let firewall_status_response = igd_service
.action(gateway.url(), "GetFirewallStatus", "")
.await
.unwrap();
let firewall_enabled: bool =
(u32::from_str(firewall_status_response.get("FirewallEnabled").unwrap()).unwrap()) != 0;
let can_create_inbound_pinhole: bool = u32::from_str(
firewall_status_response
.get("InboundPinholeAllowed")
.unwrap(),
)
.unwrap()
!= 0;
(firewall_enabled, can_create_inbound_pinhole)
}
/// Find a Gateway compatible with either IPv4 (supports WANIPConnection) or IPv6 (supports WANIPv6FirewallControl)
async fn find_gateway(ipv6_required: bool) -> Result<Device, Error> {
let search_urn: URN = if ipv6_required {
WAN_IPV6_FIREWALL_CONTROL
} else {
WAN_IP_CONNECTION
};
let discovered_devices = rupnp::discover(
&SearchTarget::URN(search_urn.clone()),
Duration::from_secs(3),
)
.await?
.filter_map(|result| async {
match result {
Ok(device) => Some(device),
Err(_) => None,
}
});
futures::pin_mut!(discovered_devices);
discovered_devices.next().await.ok_or_else(||anyhow!("Couldn't find any gateways supporting {}. Is port 1900 open for incoming connections from local networks?", search_urn.typ()))
}
/// Returns a list of IPs assigned to interfaces that are in the same subnet as a given IP
fn interface_ips_in_same_subnet_as(ip_to_match: IpAddr) -> Result<Vec<IpAddr>, Error> {
let interfaces = pnet::datalink::interfaces();
let ipnets = interfaces
.iter()
.filter_map(|interface| {
if interface
.ips
.iter()
.any(|ipnetwork| ipnetwork.contains(ip_to_match))
{
Some(interface.ips.clone())
} else {
None
}
})
.next()
.context("Couldn't find any local IPs within the same network as given IP")?;
let ips = ipnets.iter().map(|ip| ip.ip()).collect();
Ok(ips)
}
/// 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) -> Result<(Option<Ipv4Addr>,Option<Ipv6Addr>), 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()));
//get the first IPv4 and IPv6 found
let mut first_ipv4 :Option<Ipv4Addr> = None;
let mut first_ipv6 :Option<Ipv6Addr> = 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))
}

View file

@ -1,5 +1,8 @@
#![feature(ip)]
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;
@ -44,13 +47,18 @@ struct Config {
#[serde(default)]
lan_discovery: bool,
/// Forward an external port to Wiregard using UPnP IGD
upnp_forward_external_port: Option<u16>,
/// 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<Peer>,
/// Settings for the Wireguard interfaces (currently only necessary if you want to use igd features)
#[serde(default)]
interfaces: Vec<InterfaceSetting>,
#[serde(default)]
forbidden_nets: Vec<ipnet::IpNet>,
}
@ -61,13 +69,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<u16>,
/// An optional Wireguard endpoint used to initialize a connection to this peer
endpoint: Option<String>,
}
/// 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<u16>,
}
fn main() -> Result<()> {
pretty_env_logger::init();
@ -213,8 +229,8 @@ impl Daemon {
.map(|interface_name| wg_dump(&interface_name).map(|ifinfo| (interface_name, ifinfo)))
.collect::<Result<HashMap<_, _>>>()?;
let socket = UdpSocket::bind(SocketAddr::new("::".parse()?, config.gossip_port))?;
//socket.set_broadcast(true)?;
socket.set_ttl(1)?;
socket.set_broadcast(true)?;
//socket.set_ttl(1)?;
let our_pubkey = interfaces.iter().next().unwrap().1.our_pubkey.clone();
@ -302,7 +318,7 @@ impl Daemon {
error!("Wireguard configuration loop error: {}", e);
}
i = i + 1;
std::thread::sleep(TRY_INTERVAL);
thread::sleep(TRY_INTERVAL);
}
}
@ -342,7 +358,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));
}
}
}
@ -408,7 +424,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);
}
}
}
@ -425,7 +441,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);
}
}
}
@ -434,67 +450,69 @@ impl Daemon {
let packet = self.make_packet(&Gossip::LanBroadcast {
pubkey: self.our_pubkey.clone(),
})?;
let addr = 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) = self.igd_loop_iter(external_port) {
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);
}
std::thread::sleep(IGD_INTERVAL);
}
tokio::time::sleep(IGD_INTERVAL).await;
}
});
}
}
fn generate_portmaps(&self) -> (HashMap<u16,u16>, Vec<u16>) {
let mut portmap_v4: HashMap<u16, u16> = 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
}
fn igd_loop_iter(&self, external_port: u16) -> Result<()> {
let gateway = igd::search_gateway(Default::default())?;
let gwa = gateway.addr.ip().octets();
let cmplen = match gwa {
[192, 168, _, _] => 3,
[10, _, _, _] => 2,
_ => bail!(
"Gateway IP does not appear to be in a local network ({})",
gateway.addr.ip()
),
};
let private_ip = get_if_addrs::get_if_addrs()?
.into_iter()
.map(|i| i.addr.ip())
.filter_map(|a| match a {
std::net::IpAddr::V4(a4) if a4.octets()[..cmplen] == gwa[..cmplen] => Some(a4),
_ => None,
})
.next()
.ok_or(anyhow!("No interface has an IP on same subnet as gateway"))?;
debug!(
"IGD: gateway is {}, private IP is {}, making announce",
gateway.addr, private_ip
);
let ports = self
.state
.lock()
.unwrap()
.interfaces
.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()
.map(|(_, IfInfo { listen_port, .. })| *listen_port)
.collect::<Vec<_>>();
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",
)?;
.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<u16> = 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)
Ok(())
}
fn make_packet(&self, gossip: &Gossip) -> Result<Vec<u8>> {
@ -531,7 +549,7 @@ impl State {
.peers
.iter()
.filter(|(_, info)| {
let seen = now < info.last_seen + TIMEOUT.as_secs();
let seen = info.last_seen != u64::MAX && now < info.last_seen + TIMEOUT.as_secs();
let endpoint_valid = info
.endpoint
.map(|ep| {
@ -658,7 +676,7 @@ impl State {
// if peer is connected and endpoint is the correct one,
// set higher keepalive and then skip reconfiguring it
if !bad_endpoint && !forbidden_endpoint && now < peer.last_seen + TIMEOUT.as_secs()
if !bad_endpoint && peer.last_seen != u64::MAX && !forbidden_endpoint && now < peer.last_seen + TIMEOUT.as_secs()
{
Command::new("wg")
.args([
@ -725,7 +743,7 @@ impl State {
// Skip if we are already using that endpoint
continue;
}
if let Some(port) = peer_cfg.port {
info!("Configure {} with endpoint {}", peer_cfg.pubkey, endpoint);
Command::new("wg")
.args([
@ -734,7 +752,7 @@ impl State {
"peer",
&peer_cfg.pubkey,
"endpoint",
&SocketAddr::new(endpoint, peer_cfg.port).to_string(),
&SocketAddr::new(endpoint, port).to_string(),
"persistent-keepalive",
"10",
"allowed-ips",
@ -746,6 +764,7 @@ impl State {
&packet,
SocketAddr::new(peer_cfg.address, daemon.config.gossip_port),
)?;
}
} else {
info!("Configure {} with no known endpoint", peer_cfg.pubkey);
Command::new("wg")