use std::collections::BTreeMap;
use std::fmt;
use crate::netlink::genl::ovpn::messages::{OvpnKeyconf, OvpnKeydir, OvpnPeer};
use crate::netlink::genl::ovpn::types::{OvpnCipherAlg, OvpnKeySlot};
use crate::netlink::Connection;
use crate::{Error, Result};
use super::Ovpn;
#[derive(Debug, Clone, Default)]
#[must_use = "OvpnConfig does nothing unless .diff() or .apply() is called"]
pub struct OvpnConfig {
interfaces: Vec<OvpnInterfaceConfig>,
}
impl OvpnConfig {
pub fn new() -> Self {
Self::default()
}
pub fn interface<F>(mut self, ifindex: u32, build: F) -> Self
where
F: FnOnce(OvpnInterfaceConfigBuilder) -> OvpnInterfaceConfigBuilder,
{
let cfg = build(OvpnInterfaceConfigBuilder::new(ifindex)).build();
self.interfaces.push(cfg);
self
}
pub fn interfaces(&self) -> &[OvpnInterfaceConfig] {
&self.interfaces
}
pub async fn diff(&self, conn: &Connection<Ovpn>) -> Result<OvpnDiff> {
let mut diff = OvpnDiff::default();
for iface in &self.interfaces {
let live_peers = conn.peer_dump(iface.ifindex).await?;
let live_by_id: BTreeMap<u32, OvpnPeer> = live_peers
.into_iter()
.filter_map(|p| p.id.map(|id| (id, p)))
.collect();
for (&peer_id, declared) in &iface.peers {
match live_by_id.get(&peer_id) {
None => {
diff.peers_to_add
.push((iface.ifindex, peer_id, declared.clone()));
}
Some(live) => {
if !peer_matches(declared, live) {
diff.peers_to_update.push((
iface.ifindex,
peer_id,
declared.clone(),
));
}
}
}
}
for &live_id in live_by_id.keys() {
if !iface.peers.contains_key(&live_id) {
diff.peers_to_remove.push((iface.ifindex, live_id));
}
}
for (&peer_id, declared_peer) in &iface.peers {
for (&slot, declared_key) in &declared_peer.keys {
let live = conn.key_get(iface.ifindex, peer_id, slot).await;
match live {
Err(e) if e.is_not_found() => {
diff.keys_to_install.push((
iface.ifindex,
peer_id,
slot,
declared_key.clone(),
));
}
Err(other) => return Err(other),
Ok(live_meta) => {
if !key_metadata_matches(declared_key, &live_meta) {
diff.keys_to_delete
.push((iface.ifindex, peer_id, slot));
diff.keys_to_install.push((
iface.ifindex,
peer_id,
slot,
declared_key.clone(),
));
}
}
}
}
}
}
Ok(diff)
}
pub async fn apply(&self, conn: &Connection<Ovpn>) -> Result<()> {
self.diff(conn).await?.apply(conn).await
}
pub async fn apply_reconcile(&self, conn: &Connection<Ovpn>) -> Result<()> {
self.apply(conn).await?;
let post = self.diff(conn).await?;
if post.is_empty() {
return Ok(());
}
Err(Error::InvalidMessage(format!(
"ovpn apply_reconcile: kernel state diverged from config after apply \
({post})"
)))
}
}
#[derive(Debug, Clone)]
pub struct OvpnInterfaceConfig {
pub ifindex: u32,
pub peers: BTreeMap<u32, OvpnPeerConfig>,
}
#[derive(Debug)]
#[must_use = "builders do nothing unless used"]
pub struct OvpnInterfaceConfigBuilder {
ifindex: u32,
peers: BTreeMap<u32, OvpnPeerConfig>,
}
impl OvpnInterfaceConfigBuilder {
pub fn new(ifindex: u32) -> Self {
Self {
ifindex,
peers: BTreeMap::new(),
}
}
pub fn peer<F>(mut self, peer_id: u32, build: F) -> Self
where
F: FnOnce(OvpnPeerConfigBuilder) -> OvpnPeerConfigBuilder,
{
let cfg = build(OvpnPeerConfigBuilder::new(peer_id)).build();
self.peers.insert(peer_id, cfg);
self
}
pub fn build(self) -> OvpnInterfaceConfig {
OvpnInterfaceConfig {
ifindex: self.ifindex,
peers: self.peers,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OvpnPeerConfig {
pub peer_id: u32,
pub remote: Option<std::net::SocketAddr>,
pub local: Option<std::net::SocketAddr>,
pub vpn_ipv4: Option<std::net::Ipv4Addr>,
pub vpn_ipv6: Option<std::net::Ipv6Addr>,
pub keepalive_interval: Option<u32>,
pub keepalive_timeout: Option<u32>,
pub socket_fd: Option<u32>,
pub keys: BTreeMap<OvpnKeySlot, OvpnKeyConfig>,
}
#[derive(Debug)]
#[must_use = "builders do nothing unless used"]
pub struct OvpnPeerConfigBuilder {
cfg: OvpnPeerConfig,
}
impl OvpnPeerConfigBuilder {
pub fn new(peer_id: u32) -> Self {
Self {
cfg: OvpnPeerConfig {
peer_id,
..OvpnPeerConfig::default()
},
}
}
pub fn remote(mut self, addr: std::net::SocketAddr) -> Self {
self.cfg.remote = Some(addr);
self
}
pub fn local(mut self, addr: std::net::SocketAddr) -> Self {
self.cfg.local = Some(addr);
self
}
pub fn vpn_ipv4(mut self, addr: std::net::Ipv4Addr) -> Self {
self.cfg.vpn_ipv4 = Some(addr);
self
}
pub fn vpn_ipv6(mut self, addr: std::net::Ipv6Addr) -> Self {
self.cfg.vpn_ipv6 = Some(addr);
self
}
pub fn keepalive(mut self, interval_secs: u32, timeout_secs: u32) -> Self {
self.cfg.keepalive_interval = Some(interval_secs);
self.cfg.keepalive_timeout = Some(timeout_secs);
self
}
pub fn socket_fd(mut self, fd: u32) -> Self {
self.cfg.socket_fd = Some(fd);
self
}
pub fn key(mut self, slot: OvpnKeySlot, key: OvpnKeyConfig) -> Self {
self.cfg.keys.insert(slot, key);
self
}
pub fn build(self) -> OvpnPeerConfig {
self.cfg
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OvpnKeyConfig {
pub key_id: u32,
pub cipher_alg: OvpnCipherAlg,
pub encrypt: OvpnKeydir,
pub decrypt: OvpnKeydir,
}
impl OvpnKeyConfig {
pub fn new(
key_id: u32,
cipher_alg: OvpnCipherAlg,
encrypt: OvpnKeydir,
decrypt: OvpnKeydir,
) -> Self {
Self {
key_id,
cipher_alg,
encrypt,
decrypt,
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct OvpnDiff {
pub peers_to_add: Vec<(u32, u32, OvpnPeerConfig)>,
pub peers_to_update: Vec<(u32, u32, OvpnPeerConfig)>,
pub peers_to_remove: Vec<(u32, u32)>,
pub keys_to_install: Vec<(u32, u32, OvpnKeySlot, OvpnKeyConfig)>,
pub keys_to_delete: Vec<(u32, u32, OvpnKeySlot)>,
}
impl OvpnDiff {
pub fn is_empty(&self) -> bool {
self.peers_to_add.is_empty()
&& self.peers_to_update.is_empty()
&& self.peers_to_remove.is_empty()
&& self.keys_to_install.is_empty()
&& self.keys_to_delete.is_empty()
}
pub fn summary(&self) -> String {
format!(
"+{}p ~{}p -{}p +{}k -{}k",
self.peers_to_add.len(),
self.peers_to_update.len(),
self.peers_to_remove.len(),
self.keys_to_install.len(),
self.keys_to_delete.len(),
)
}
pub async fn apply(&self, conn: &Connection<Ovpn>) -> Result<()> {
for (ifindex, peer_id) in &self.peers_to_remove {
conn.peer_del(*ifindex, *peer_id).await?;
}
for (ifindex, peer_id, slot) in &self.keys_to_delete {
conn.key_del(*ifindex, *peer_id, *slot).await?;
}
for (ifindex, peer_id, peer) in &self.peers_to_add {
conn.peer_new(*ifindex, build_kernel_peer(*peer_id, peer))
.await?;
}
for (ifindex, peer_id, peer) in &self.peers_to_update {
conn.peer_set(*ifindex, build_kernel_peer(*peer_id, peer))
.await?;
}
for (ifindex, peer_id, slot, key) in &self.keys_to_install {
let keyconf = OvpnKeyconf::new(
*peer_id,
*slot,
key.key_id,
key.cipher_alg,
key.encrypt.clone(),
key.decrypt.clone(),
);
conn.key_new(*ifindex, keyconf).await?;
}
Ok(())
}
}
impl fmt::Display for OvpnDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("(no changes)");
}
let mut first = true;
let mut write_section = |label: &str, count: usize| -> fmt::Result {
if count == 0 {
return Ok(());
}
if !first {
f.write_str(", ")?;
}
first = false;
write!(f, "{count} {label}")
};
write_section("peers to add", self.peers_to_add.len())?;
write_section("peers to update", self.peers_to_update.len())?;
write_section("peers to remove", self.peers_to_remove.len())?;
write_section("keys to install", self.keys_to_install.len())?;
write_section("keys to delete", self.keys_to_delete.len())?;
Ok(())
}
}
fn build_kernel_peer(peer_id: u32, cfg: &OvpnPeerConfig) -> OvpnPeer {
let mut peer = OvpnPeer::identity(peer_id);
if let Some(remote) = cfg.remote {
match remote {
std::net::SocketAddr::V4(v4) => peer.set_remote_v4(v4),
std::net::SocketAddr::V6(v6) => peer.set_remote_v6(v6),
}
}
if let Some(local) = cfg.local {
match local {
std::net::SocketAddr::V4(v4) => {
peer.local_ipv4 = Some(OvpnPeer::encode_ipv4(*v4.ip()));
peer.local_port = Some(OvpnPeer::encode_port(v4.port()));
}
std::net::SocketAddr::V6(v6) => {
peer.local_ipv6 = Some(OvpnPeer::encode_ipv6(*v6.ip()));
peer.local_port = Some(OvpnPeer::encode_port(v6.port()));
}
}
}
if let Some(v4) = cfg.vpn_ipv4 {
peer.vpn_ipv4 = Some(OvpnPeer::encode_ipv4(v4));
}
if let Some(v6) = cfg.vpn_ipv6 {
peer.vpn_ipv6 = Some(OvpnPeer::encode_ipv6(v6));
}
if let Some(interval) = cfg.keepalive_interval {
peer.keepalive_interval = Some(interval);
}
if let Some(timeout) = cfg.keepalive_timeout {
peer.keepalive_timeout = Some(timeout);
}
if let Some(fd) = cfg.socket_fd {
peer.socket = Some(fd);
}
peer
}
fn peer_matches(declared: &OvpnPeerConfig, live: &OvpnPeer) -> bool {
if let Some(remote) = declared.remote
&& live.remote_socket() != Some(remote)
{
return false;
}
if let Some(v4) = declared.vpn_ipv4
&& live.vpn_ipv4.as_deref().and_then(OvpnPeer::decode_ipv4) != Some(v4)
{
return false;
}
if let Some(v6) = declared.vpn_ipv6
&& live.vpn_ipv6.as_deref().and_then(OvpnPeer::decode_ipv6) != Some(v6)
{
return false;
}
if let Some(interval) = declared.keepalive_interval
&& live.keepalive_interval != Some(interval)
{
return false;
}
if let Some(timeout) = declared.keepalive_timeout
&& live.keepalive_timeout != Some(timeout)
{
return false;
}
true
}
fn key_metadata_matches(declared: &OvpnKeyConfig, live: &OvpnKeyconf) -> bool {
if live.cipher_alg != Some(declared.cipher_alg) {
return false;
}
if live.key_id != Some(declared.key_id) {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_key(key_id: u32) -> OvpnKeyConfig {
OvpnKeyConfig::new(
key_id,
OvpnCipherAlg::AesGcm,
OvpnKeydir::new([1u8; 32], [2u8; 8]),
OvpnKeydir::new([3u8; 32], [4u8; 8]),
)
}
#[test]
fn empty_diff_summary_is_descriptive() {
let diff = OvpnDiff::default();
assert!(diff.is_empty());
assert_eq!(diff.summary(), "+0p ~0p -0p +0k -0k");
assert_eq!(format!("{diff}"), "(no changes)");
}
#[test]
fn diff_display_lists_nonzero_sections() {
let mut diff = OvpnDiff::default();
diff.peers_to_add
.push((7, 1, OvpnPeerConfig::default()));
diff.keys_to_install.push((
7,
1,
OvpnKeySlot::Primary,
dummy_key(0),
));
let s = format!("{diff}");
assert!(s.contains("1 peers to add"), "{s}");
assert!(s.contains("1 keys to install"), "{s}");
assert!(!s.contains("peers to remove"), "{s}");
}
#[test]
fn config_builder_records_peer() {
let cfg = OvpnConfig::new()
.interface(5, |b| {
b.peer(42, |p| {
p.remote("10.0.0.1:1194".parse().unwrap())
.keepalive(20, 60)
.vpn_ipv4("172.16.0.1".parse().unwrap())
.key(OvpnKeySlot::Primary, dummy_key(1))
})
})
.interface(5, |b| {
b.peer(99, |p| p.keepalive(30, 90))
});
assert_eq!(cfg.interfaces().len(), 2);
assert_eq!(cfg.interfaces()[0].ifindex, 5);
assert_eq!(cfg.interfaces()[0].peers.len(), 1);
let p = &cfg.interfaces()[0].peers[&42];
assert_eq!(p.peer_id, 42);
assert_eq!(p.keepalive_interval, Some(20));
assert_eq!(p.keys.len(), 1);
assert_eq!(
p.keys.get(&OvpnKeySlot::Primary).unwrap().cipher_alg,
OvpnCipherAlg::AesGcm
);
}
#[test]
fn peer_matches_ignores_counters() {
let declared = OvpnPeerConfig {
peer_id: 1,
remote: Some("10.0.0.1:1194".parse().unwrap()),
keepalive_interval: Some(20),
keepalive_timeout: Some(60),
..OvpnPeerConfig::default()
};
let mut live = OvpnPeer::identity(1);
live.set_remote_v4("10.0.0.1:1194".parse().unwrap());
live.keepalive_interval = Some(20);
live.keepalive_timeout = Some(60);
live.vpn_rx_bytes = Some(1_000_000_000);
live.link_tx_packets = Some(50_000);
assert!(peer_matches(&declared, &live));
}
#[test]
fn peer_matches_detects_endpoint_drift() {
let declared = OvpnPeerConfig {
peer_id: 1,
remote: Some("10.0.0.1:1194".parse().unwrap()),
..OvpnPeerConfig::default()
};
let mut live = OvpnPeer::identity(1);
live.set_remote_v4("10.0.0.2:1194".parse().unwrap());
assert!(!peer_matches(&declared, &live));
}
#[test]
fn peer_matches_detects_keepalive_drift() {
let declared = OvpnPeerConfig {
peer_id: 1,
keepalive_interval: Some(20),
..OvpnPeerConfig::default()
};
let mut live = OvpnPeer::identity(1);
live.keepalive_interval = Some(30);
assert!(!peer_matches(&declared, &live));
}
#[test]
fn key_metadata_matches_on_alg_and_id() {
let declared = dummy_key(1);
let live = OvpnKeyconf {
peer_id: Some(7),
slot: Some(OvpnKeySlot::Primary),
key_id: Some(1),
cipher_alg: Some(OvpnCipherAlg::AesGcm),
..OvpnKeyconf::default()
};
assert!(key_metadata_matches(&declared, &live));
}
#[test]
fn key_metadata_mismatch_on_alg() {
let declared = dummy_key(1);
let live = OvpnKeyconf {
peer_id: Some(7),
slot: Some(OvpnKeySlot::Primary),
key_id: Some(1),
cipher_alg: Some(OvpnCipherAlg::Chacha20Poly1305),
..OvpnKeyconf::default()
};
assert!(!key_metadata_matches(&declared, &live));
}
#[test]
fn key_metadata_mismatch_on_key_id() {
let declared = dummy_key(1);
let live = OvpnKeyconf {
peer_id: Some(7),
slot: Some(OvpnKeySlot::Primary),
key_id: Some(2),
cipher_alg: Some(OvpnCipherAlg::AesGcm),
..OvpnKeyconf::default()
};
assert!(!key_metadata_matches(&declared, &live));
}
#[test]
fn build_kernel_peer_serializes_endpoint_correctly() {
let cfg = OvpnPeerConfig {
peer_id: 7,
remote: Some("10.0.0.1:1194".parse().unwrap()),
vpn_ipv4: Some("172.16.0.7".parse().unwrap()),
keepalive_interval: Some(20),
keepalive_timeout: Some(60),
..OvpnPeerConfig::default()
};
let peer = build_kernel_peer(7, &cfg);
assert_eq!(peer.id, Some(7));
assert_eq!(
peer.remote_socket(),
Some("10.0.0.1:1194".parse().unwrap())
);
assert_eq!(
peer.vpn_ipv4.as_deref().and_then(OvpnPeer::decode_ipv4),
Some("172.16.0.7".parse().unwrap())
);
assert_eq!(peer.keepalive_interval, Some(20));
assert_eq!(peer.keepalive_timeout, Some(60));
}
}