Compare commits
3 commits
main
...
local_ipv6
Author | SHA1 | Date | |
---|---|---|---|
|
c5d6840e27 | ||
|
6614dfaeb6 | ||
|
6147bb0b8c |
6 changed files with 119 additions and 29 deletions
11
README.md
11
README.md
|
@ -5,9 +5,14 @@ Diplonat
|
||||||
|
|
||||||
## Feature set
|
## Feature set
|
||||||
|
|
||||||
* [X] (Re)Configure NAT via UPNP/IGD (prio: high)
|
Diplonat performs two main tasks:
|
||||||
* [X] (Re)Configure iptables (prio: low)
|
|
||||||
* [ ] (Re)Configure DNS via ??? (prio: low)
|
1) ensure that all services are accessible from the Internet
|
||||||
|
- it detects services by watching Consul and looking for a special "diplonat" tag (see below)
|
||||||
|
- for each service, it configures the host firewall with iptables and the router NAT with IGD
|
||||||
|
2) autodiscovery of the public IP addresses of the local node
|
||||||
|
- it uses STUN to a remote server for IPv4, and looks locally for a usable IPv6 address
|
||||||
|
- it then writes the discovered IP addresses to Consul, so that other services can use them (D53 for the DNS, Garage, Jitsi...)
|
||||||
|
|
||||||
## Understand scope
|
## Understand scope
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, IpAddr::V6, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::config::{RuntimeConfigConsul, RuntimeConfigStun};
|
use crate::config::{RuntimeConfigAutoDiscovery, RuntimeConfigConsul};
|
||||||
use crate::consul;
|
use crate::consul;
|
||||||
|
|
||||||
/// If autodiscovery returns None but an address was obtained less than
|
/// If autodiscovery returns None but an address was obtained less than
|
||||||
|
@ -13,12 +13,12 @@ use crate::consul;
|
||||||
/// in the Consul db instead of insterting a None.
|
/// in the Consul db instead of insterting a None.
|
||||||
const PERSIST_SOME_RESULT_DURATION_SECS: u64 = 900;
|
const PERSIST_SOME_RESULT_DURATION_SECS: u64 = 900;
|
||||||
|
|
||||||
pub struct StunActor {
|
pub struct AutoDiscoveryActor {
|
||||||
consul: consul::Consul,
|
consul: consul::Consul,
|
||||||
refresh_time: Duration,
|
refresh_time: Duration,
|
||||||
|
|
||||||
autodiscovery_v4: StunAutodiscovery,
|
autodiscovery_v4: StunAutodiscovery,
|
||||||
autodiscovery_v6: StunAutodiscovery,
|
autodiscovery_v6: Localv6AddressAutodiscovery,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StunAutodiscovery {
|
pub struct StunAutodiscovery {
|
||||||
|
@ -28,35 +28,38 @@ pub struct StunAutodiscovery {
|
||||||
last_result: Option<AutodiscoverResult>,
|
last_result: Option<AutodiscoverResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Localv6AddressAutodiscovery {
|
||||||
|
consul_key: String,
|
||||||
|
last_result: Option<AutodiscoverResult>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct AutodiscoverResult {
|
pub struct AutodiscoverResult {
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub address: Option<IpAddr>,
|
pub address: Option<IpAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StunActor {
|
impl AutoDiscoveryActor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
consul_config: &RuntimeConfigConsul,
|
consul_config: &RuntimeConfigConsul,
|
||||||
stun_config: &RuntimeConfigStun,
|
autodiscovery_config: &RuntimeConfigAutoDiscovery,
|
||||||
node: &str,
|
node: &str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
assert!(stun_config
|
assert!(autodiscovery_config
|
||||||
.stun_server_v4
|
.stun_server_v4
|
||||||
.map(|x| x.is_ipv4())
|
.map(|x| x.is_ipv4())
|
||||||
.unwrap_or(true));
|
.unwrap_or(true));
|
||||||
assert!(stun_config.stun_server_v6.is_ipv6());
|
assert!(autodiscovery_config.stun_server_v6.is_ipv6());
|
||||||
|
|
||||||
let autodiscovery_v4 = StunAutodiscovery {
|
let autodiscovery_v4 = StunAutodiscovery {
|
||||||
consul_key: format!("diplonat/autodiscovery/ipv4/{}", node),
|
consul_key: format!("diplonat/autodiscovery/ipv4/{}", node),
|
||||||
is_ipv4: true,
|
is_ipv4: true,
|
||||||
stun_server: stun_config.stun_server_v4,
|
stun_server: autodiscovery_config.stun_server_v4,
|
||||||
last_result: None,
|
last_result: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let autodiscovery_v6 = StunAutodiscovery {
|
let autodiscovery_v6 = Localv6AddressAutodiscovery {
|
||||||
consul_key: format!("diplonat/autodiscovery/ipv6/{}", node),
|
consul_key: format!("diplonat/autodiscovery/ipv6/{}", node),
|
||||||
is_ipv4: false,
|
|
||||||
stun_server: Some(stun_config.stun_server_v6),
|
|
||||||
last_result: None,
|
last_result: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,7 +67,7 @@ impl StunActor {
|
||||||
consul: consul::Consul::new(consul_config),
|
consul: consul::Consul::new(consul_config),
|
||||||
autodiscovery_v4,
|
autodiscovery_v4,
|
||||||
autodiscovery_v6,
|
autodiscovery_v6,
|
||||||
refresh_time: stun_config.refresh_time,
|
refresh_time: autodiscovery_config.refresh_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +171,81 @@ async fn get_mapped_addr(
|
||||||
Ok(Some(xor_mapped_addr))
|
Ok(Some(xor_mapped_addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Localv6AddressAutodiscovery {
|
||||||
|
async fn do_iteration(&mut self, consul: &consul::Consul) -> Result<()> {
|
||||||
|
let now = timestamp();
|
||||||
|
|
||||||
|
let discovered_addr = match get_kernel_src_ipv6_addr() {
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(last_result) = &self.last_result {
|
||||||
|
if last_result.address.is_some()
|
||||||
|
&& now - last_result.timestamp <= PERSIST_SOME_RESULT_DURATION_SECS
|
||||||
|
{
|
||||||
|
// Keep non-None result that was obtained before by not
|
||||||
|
// writing/taking into account None result.
|
||||||
|
info!("Temporarily failed to determine IPv6 address (error: {}), keeping existing address in Consul ", e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error!(
|
||||||
|
"Failed to determine IPv6 address (error: {}), removing from Consul",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(addr) => Some(addr),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_result = AutodiscoverResult {
|
||||||
|
timestamp: now,
|
||||||
|
address: discovered_addr,
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = format!(
|
||||||
|
"Local IPv6 address autodiscovery result: {} -> {:?}",
|
||||||
|
self.consul_key, discovered_addr
|
||||||
|
);
|
||||||
|
if self.last_result.as_ref().and_then(|x| x.address) != discovered_addr {
|
||||||
|
info!("{}", msg);
|
||||||
|
} else {
|
||||||
|
debug!("{}", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
consul
|
||||||
|
.kv_put(&self.consul_key, serde_json::to_vec(¤t_result)?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.last_result = Some(current_result);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_kernel_src_ipv6_addr() -> Result<IpAddr> {
|
||||||
|
let binding_ip = IpAddr::V6(Ipv6Addr::UNSPECIFIED);
|
||||||
|
let binding_addr = SocketAddr::new(binding_ip, 0);
|
||||||
|
let socket = UdpSocket::bind(binding_addr)?;
|
||||||
|
|
||||||
|
// Old trick: connecting a UDP socket does not actually send any
|
||||||
|
// packet, but it forces the kernel to determine the correct
|
||||||
|
// source IP address.
|
||||||
|
socket.connect("[2001::1]:4242")?;
|
||||||
|
|
||||||
|
let discovered_addr = socket.local_addr().and_then(|x| Ok(x.ip()))?;
|
||||||
|
|
||||||
|
// Filter out unwanted addresses (localhost, unspecified, link-local)
|
||||||
|
// TODO: use is_global() in the future: https://doc.rust-lang.org/std/net/struct.Ipv6Addr.html#method.is_global
|
||||||
|
match discovered_addr {
|
||||||
|
V6(addr) if addr.is_loopback() => Err(anyhow!("ignoring loopback address {}", addr)),
|
||||||
|
V6(addr) if addr.is_unspecified() => Err(anyhow!("ignoring unspecified address {}", addr)),
|
||||||
|
// Reimplement is_unicast_link_local (only available with Rust 1.84)
|
||||||
|
V6(addr) if (addr.segments()[0] & 0xffc0) == 0xfe80 => {
|
||||||
|
Err(anyhow!("ignoring link-local address {}", addr))
|
||||||
|
}
|
||||||
|
addr => Ok(addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn timestamp() -> u64 {
|
fn timestamp() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
@ -5,7 +5,8 @@ mod runtime;
|
||||||
|
|
||||||
pub use options::{ConfigOpts, ConfigOptsBase, ConfigOptsConsul};
|
pub use options::{ConfigOpts, ConfigOptsBase, ConfigOptsConsul};
|
||||||
pub use runtime::{
|
pub use runtime::{
|
||||||
RuntimeConfig, RuntimeConfigConsul, RuntimeConfigFirewall, RuntimeConfigIgd, RuntimeConfigStun,
|
RuntimeConfig, RuntimeConfigAutoDiscovery, RuntimeConfigConsul, RuntimeConfigFirewall,
|
||||||
|
RuntimeConfigIgd,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const EXPIRATION_TIME: u16 = 300;
|
pub const EXPIRATION_TIME: u16 = 300;
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub struct RuntimeConfigIgd {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RuntimeConfigStun {
|
pub struct RuntimeConfigAutoDiscovery {
|
||||||
pub stun_server_v4: Option<SocketAddr>,
|
pub stun_server_v4: Option<SocketAddr>,
|
||||||
pub stun_server_v6: SocketAddr,
|
pub stun_server_v6: SocketAddr,
|
||||||
pub refresh_time: Duration,
|
pub refresh_time: Duration,
|
||||||
|
@ -44,7 +44,7 @@ pub struct RuntimeConfig {
|
||||||
pub consul: RuntimeConfigConsul,
|
pub consul: RuntimeConfigConsul,
|
||||||
pub firewall: RuntimeConfigFirewall,
|
pub firewall: RuntimeConfigFirewall,
|
||||||
pub igd: Option<RuntimeConfigIgd>,
|
pub igd: Option<RuntimeConfigIgd>,
|
||||||
pub stun: RuntimeConfigStun,
|
pub autodiscovery: RuntimeConfigAutoDiscovery,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeConfig {
|
impl RuntimeConfig {
|
||||||
|
@ -55,13 +55,13 @@ impl RuntimeConfig {
|
||||||
false => Some(RuntimeConfigIgd::new(&opts.base)?),
|
false => Some(RuntimeConfigIgd::new(&opts.base)?),
|
||||||
true => None,
|
true => None,
|
||||||
};
|
};
|
||||||
let stun = RuntimeConfigStun::new(&opts.base)?;
|
let autodiscovery = RuntimeConfigAutoDiscovery::new(&opts.base)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
consul,
|
consul,
|
||||||
firewall,
|
firewall,
|
||||||
igd,
|
igd,
|
||||||
stun,
|
autodiscovery,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ impl RuntimeConfigIgd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeConfigStun {
|
impl RuntimeConfigAutoDiscovery {
|
||||||
pub(super) fn new(opts: &ConfigOptsBase) -> Result<Self> {
|
pub(super) fn new(opts: &ConfigOptsBase) -> Result<Self> {
|
||||||
let mut stun_server_v4 = None;
|
let mut stun_server_v4 = None;
|
||||||
let mut stun_server_v6 = None;
|
let mut stun_server_v6 = None;
|
||||||
|
|
|
@ -3,15 +3,15 @@ use futures::future::FutureExt;
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ConfigOpts, consul_actor::ConsulActor, fw_actor::FirewallActor, igd_actor::IgdActor,
|
autodiscovery_actor::AutoDiscoveryActor, config::ConfigOpts, consul_actor::ConsulActor,
|
||||||
stun_actor::StunActor,
|
fw_actor::FirewallActor, igd_actor::IgdActor,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Diplonat {
|
pub struct Diplonat {
|
||||||
consul: ConsulActor,
|
consul: ConsulActor,
|
||||||
firewall: FirewallActor,
|
firewall: FirewallActor,
|
||||||
igd: Option<IgdActor>,
|
igd: Option<IgdActor>,
|
||||||
stun: StunActor,
|
autodiscovery: AutoDiscoveryActor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diplonat {
|
impl Diplonat {
|
||||||
|
@ -43,13 +43,17 @@ impl Diplonat {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let sa = StunActor::new(&rt_cfg.consul, &rt_cfg.stun, &rt_cfg.consul.node_name);
|
let ad = AutoDiscoveryActor::new(
|
||||||
|
&rt_cfg.consul,
|
||||||
|
&rt_cfg.autodiscovery,
|
||||||
|
&rt_cfg.consul.node_name,
|
||||||
|
);
|
||||||
|
|
||||||
let ctx = Self {
|
let ctx = Self {
|
||||||
consul: ca,
|
consul: ca,
|
||||||
igd: ia,
|
igd: ia,
|
||||||
firewall: fw,
|
firewall: fw,
|
||||||
stun: sa,
|
autodiscovery: ad,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
|
@ -70,7 +74,9 @@ impl Diplonat {
|
||||||
self.firewall
|
self.firewall
|
||||||
.listen()
|
.listen()
|
||||||
.map(|x| x.context("Run firewall actor")),
|
.map(|x| x.context("Run firewall actor")),
|
||||||
self.stun.listen().map(|x| x.context("Run STUN actor")),
|
self.autodiscovery
|
||||||
|
.listen()
|
||||||
|
.map(|x| x.context("Run autodiscovery actor")),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod autodiscovery_actor;
|
||||||
mod config;
|
mod config;
|
||||||
mod consul;
|
mod consul;
|
||||||
mod consul_actor;
|
mod consul_actor;
|
||||||
|
@ -6,7 +7,6 @@ mod fw;
|
||||||
mod fw_actor;
|
mod fw_actor;
|
||||||
mod igd_actor;
|
mod igd_actor;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod stun_actor;
|
|
||||||
|
|
||||||
use diplonat::Diplonat;
|
use diplonat::Diplonat;
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
Loading…
Add table
Reference in a new issue