use crate::destination::Destination;
use crate::error::I2pError;
use std::ffi::CString;
use std::os::raw::c_int;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
static ROUTER_RUNNING: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum SigType {
DsaSha1,
EcdsaP256,
EcdsaP384,
EcdsaP521,
#[default]
Eddsa25519,
}
impl SigType {
pub(crate) const fn as_raw(self) -> c_int {
match self {
Self::DsaSha1 => 0,
Self::EcdsaP256 => 1,
Self::EcdsaP384 => 2,
Self::EcdsaP521 => 3,
Self::Eddsa25519 => 7,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CryptoType {
ElGamal,
EciesP256,
#[default]
EciesX25519,
EciesMlkem512X25519,
EciesMlkem768X25519,
EciesMlkem1024X25519,
}
impl CryptoType {
pub(crate) const fn as_raw(self) -> c_int {
match self {
Self::ElGamal => 0,
Self::EciesP256 => 1,
Self::EciesX25519 => 4,
Self::EciesMlkem512X25519 => 5,
Self::EciesMlkem768X25519 => 6,
Self::EciesMlkem1024X25519 => 7,
}
}
#[must_use]
pub fn is_supported(self) -> bool {
let mlkem_variant = match self {
Self::ElGamal | Self::EciesP256 | Self::EciesX25519 => return true,
Self::EciesMlkem512X25519 => 0,
Self::EciesMlkem768X25519 => 1,
Self::EciesMlkem1024X25519 => 2,
};
unsafe { i2pd_sys::i2pd_test_mlkem_roundtrip(mlkem_variant) != 0 }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RouterConfig {
accepts_transit: bool,
bandwidth_limit_kbps: Option<u32>,
transit_share_percent: u8,
max_transit_tunnels: Option<u32>,
floodfill: bool,
}
impl Default for RouterConfig {
fn default() -> Self {
Self {
accepts_transit: true,
bandwidth_limit_kbps: None,
transit_share_percent: 100,
max_transit_tunnels: None,
floodfill: false,
}
}
}
impl RouterConfig {
#[must_use]
pub const fn accepts_transit(mut self, enabled: bool) -> Self {
self.accepts_transit = enabled;
self
}
#[must_use]
pub const fn bandwidth_limit_kbps(mut self, kbps: u32) -> Self {
self.bandwidth_limit_kbps = Some(kbps);
self
}
#[must_use]
pub const fn transit_share_percent(mut self, percent: u8) -> Self {
self.transit_share_percent = percent;
self
}
#[must_use]
pub const fn max_transit_tunnels(mut self, max: u32) -> Self {
self.max_transit_tunnels = Some(max);
self
}
#[must_use]
pub const fn floodfill(mut self, enabled: bool) -> Self {
self.floodfill = enabled;
self
}
fn apply(&self) {
unsafe {
i2pd_sys::i2pd_set_accepts_transit(c_int::from(self.accepts_transit));
i2pd_sys::i2pd_set_bandwidth_limit(clamp_to_c_int(self.bandwidth_limit_kbps));
i2pd_sys::i2pd_set_share_percent(c_int::from(self.transit_share_percent));
i2pd_sys::i2pd_set_max_transit_tunnels(clamp_to_c_int(self.max_transit_tunnels));
i2pd_sys::i2pd_set_floodfill(c_int::from(self.floodfill));
}
}
}
const fn clamp_to_c_int(value: Option<u32>) -> c_int {
match value {
None => 0,
Some(v) if v > c_int::MAX as u32 => c_int::MAX,
Some(v) => v as c_int,
}
}
struct SecretBytes(Vec<u8>);
impl Drop for SecretBytes {
fn drop(&mut self) {
for b in &mut self.0 {
unsafe { std::ptr::write_volatile(b, 0) };
}
std::sync::atomic::compiler_fence(Ordering::SeqCst);
}
}
impl std::fmt::Debug for SecretBytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SecretBytes({} bytes)", self.0.len())
}
}
async fn write_keys_file(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent).await?;
}
let file_name = path.file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"keys file path has no file name",
)
})?;
static TMP_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let unique = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp_path = path.with_file_name(format!(
".{}.{}.{unique}.tmp",
file_name.to_string_lossy(),
std::process::id(),
));
let mut opts = tokio::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
opts.mode(0o600);
let write_result = async {
let mut file = opts.open(&tmp_path).await?;
tokio::io::AsyncWriteExt::write_all(&mut file, bytes).await?;
file.sync_all().await?;
drop(file);
tokio::fs::rename(&tmp_path, path).await
}
.await;
if write_result.is_err() {
drop(tokio::fs::remove_file(&tmp_path).await);
}
write_result
}
#[derive(Debug)]
struct RouterInner {
keys_file_lock: tokio::sync::Mutex<()>,
}
impl Drop for RouterInner {
fn drop(&mut self) {
unsafe {
i2pd_sys::i2pd_terminate();
}
ROUTER_RUNNING.store(false, Ordering::Release);
}
}
#[derive(Clone, Debug)]
pub struct I2pRouter {
_inner: Arc<RouterInner>,
}
impl I2pRouter {
pub async fn start(app_name: impl Into<String>) -> Result<Self, I2pError> {
Self::start_with_config(app_name, RouterConfig::default()).await
}
pub async fn start_with_config(
app_name: impl Into<String>,
config: RouterConfig,
) -> Result<Self, I2pError> {
let c_name = CString::new(app_name.into()).map_err(|_| I2pError::InvalidAppName)?;
if ROUTER_RUNNING.swap(true, Ordering::AcqRel) {
return Err(I2pError::AlreadyRunning);
}
let result = tokio::task::spawn_blocking(move || {
unsafe {
i2pd_sys::i2pd_init(c_name.as_ptr());
config.apply();
i2pd_sys::i2pd_start();
}
})
.await;
match result {
Ok(()) => Ok(Self {
_inner: Arc::new(RouterInner {
keys_file_lock: tokio::sync::Mutex::new(()),
}),
}),
Err(_) => Err(I2pError::WorkerPanicked),
}
}
#[must_use]
pub fn supports_transit() -> bool {
unsafe { i2pd_sys::i2pd_accepts_transit() != 0 }
}
pub async fn create_transient_destination(&self) -> Result<Destination, I2pError> {
let router = self.clone();
tokio::task::spawn_blocking(move || {
let ptr = unsafe { i2pd_sys::i2pd_create_transient_destination() };
Destination::from_raw(router, ptr)
})
.await
.map_err(|_| I2pError::WorkerPanicked)?
}
pub async fn generate_keys(&self, sig: SigType) -> Result<Vec<u8>, I2pError> {
tokio::task::spawn_blocking(move || {
let mut buf: *mut u8 = std::ptr::null_mut();
let mut len: usize = 0;
let ok = unsafe {
i2pd_sys::i2pd_generate_keys(
sig.as_raw(),
CryptoType::ElGamal.as_raw(),
&raw mut buf,
&raw mut len,
)
};
if ok == 0 || buf.is_null() {
return Err(I2pError::KeyGenerationFailed);
}
let bytes = unsafe { std::slice::from_raw_parts(buf, len) }.to_vec();
unsafe { i2pd_sys::i2pd_free_buffer(buf, len) };
Ok(bytes)
})
.await
.map_err(|_| I2pError::WorkerPanicked)?
}
pub async fn create_persistent_destination(
&self,
keys: Vec<u8>,
is_public: bool,
encryption_types: &[CryptoType],
) -> Result<Destination, I2pError> {
let router = self.clone();
let csv = if encryption_types.is_empty() {
None
} else {
Some(
encryption_types
.iter()
.map(|c| c.as_raw().to_string())
.collect::<Vec<_>>()
.join(","),
)
};
let csv = csv.and_then(|s| std::ffi::CString::new(s).ok());
let keys = SecretBytes(keys);
tokio::task::spawn_blocking(move || {
let csv_ptr = csv.as_deref().map_or(std::ptr::null(), |c| c.as_ptr());
let ptr = unsafe {
i2pd_sys::i2pd_create_persistent_destination(
keys.0.as_ptr(),
keys.0.len(),
c_int::from(is_public),
csv_ptr,
)
};
Destination::from_raw(router, ptr)
})
.await
.map_err(|_| I2pError::WorkerPanicked)?
}
pub async fn destination_from_keys_file(
&self,
path: impl Into<PathBuf>,
is_public: bool,
sig: SigType,
encryption_types: &[CryptoType],
) -> Result<Destination, I2pError> {
let path = path.into();
let mut keys = {
let _guard = self._inner.keys_file_lock.lock().await;
match tokio::fs::read(&path).await {
Ok(bytes) if bytes.is_empty() => {
return Err(I2pError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"keys file is empty",
)));
}
Ok(bytes) => SecretBytes(bytes),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let generated = SecretBytes(self.generate_keys(sig).await?);
write_keys_file(&path, &generated.0)
.await
.map_err(I2pError::Io)?;
generated
}
Err(e) => return Err(I2pError::Io(e)),
}
};
let bytes = std::mem::take(&mut keys.0);
self.create_persistent_destination(bytes, is_public, encryption_types)
.await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::{CryptoType, RouterConfig, SecretBytes, SigType, clamp_to_c_int, write_keys_file};
use std::os::raw::c_int;
#[test]
fn sig_type_raw_values() {
assert_eq!(SigType::DsaSha1.as_raw(), 0);
assert_eq!(SigType::EcdsaP256.as_raw(), 1);
assert_eq!(SigType::EcdsaP384.as_raw(), 2);
assert_eq!(SigType::EcdsaP521.as_raw(), 3);
assert_eq!(SigType::Eddsa25519.as_raw(), 7);
assert_eq!(SigType::default(), SigType::Eddsa25519);
}
#[test]
fn crypto_type_raw_values() {
assert_eq!(CryptoType::ElGamal.as_raw(), 0);
assert_eq!(CryptoType::EciesP256.as_raw(), 1);
assert_eq!(CryptoType::EciesX25519.as_raw(), 4);
assert_eq!(CryptoType::EciesMlkem512X25519.as_raw(), 5);
assert_eq!(CryptoType::EciesMlkem768X25519.as_raw(), 6);
assert_eq!(CryptoType::EciesMlkem1024X25519.as_raw(), 7);
assert_eq!(CryptoType::default(), CryptoType::EciesX25519);
}
#[test]
fn router_config_defaults() {
let config = RouterConfig::default();
assert!(config.accepts_transit);
assert!(!config.floodfill);
assert_eq!(config.bandwidth_limit_kbps, None);
assert_eq!(config.max_transit_tunnels, None);
assert_eq!(config.transit_share_percent, 100);
}
#[test]
fn router_config_builder_applies_each_setting() {
let config = RouterConfig::default()
.accepts_transit(false)
.bandwidth_limit_kbps(512)
.transit_share_percent(25)
.max_transit_tunnels(500)
.floodfill(true);
assert!(!config.accepts_transit);
assert_eq!(config.bandwidth_limit_kbps, Some(512));
assert_eq!(config.transit_share_percent, 25);
assert_eq!(config.max_transit_tunnels, Some(500));
assert!(config.floodfill);
}
#[test]
fn oversized_limits_saturate() {
assert_eq!(clamp_to_c_int(None), 0);
assert_eq!(clamp_to_c_int(Some(512)), 512);
assert_eq!(clamp_to_c_int(Some(u32::MAX)), c_int::MAX);
assert!(clamp_to_c_int(Some(u32::MAX)) > 0);
}
#[tokio::test]
async fn keys_file_is_owner_only_and_leaves_no_temporary() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("server.keys");
write_keys_file(&path, b"secret key material")
.await
.unwrap();
assert_eq!(
tokio::fs::read(&path).await.unwrap(),
b"secret key material"
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mode = tokio::fs::metadata(&path)
.await
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
}
let mut entries = tokio::fs::read_dir(dir.path()).await.unwrap();
let mut names = Vec::new();
while let Some(entry) = entries.next_entry().await.unwrap() {
names.push(entry.file_name().to_string_lossy().into_owned());
}
assert_eq!(names, vec!["server.keys".to_string()]);
}
#[tokio::test]
async fn keys_file_creates_missing_parent_directories() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested/deeper/server.keys");
write_keys_file(&path, b"secret key material")
.await
.unwrap();
assert!(path.exists());
}
#[test]
fn secret_bytes_debug_hides_contents() {
let secret = SecretBytes(vec![0xAB; 4]);
let rendered = format!("{secret:?}");
assert_eq!(rendered, "SecretBytes(4 bytes)");
assert!(!rendered.contains("171") && !rendered.to_lowercase().contains("ab"));
}
}