diff --git a/tools/relay-server/Cargo.toml b/tools/relay-server/Cargo.toml new file mode 100644 index 000000000..2f86dba75 --- /dev/null +++ b/tools/relay-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "relay-server" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rust-ipfs = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-util = { workspace = true, features = ["full"] } +tokio-stream = { workspace = true, features = ["net"] } +futures.workspace = true +futures-timer.workspace = true +async-trait.workspace = true +async-stream.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +void.workspace = true +tracing.workspace = true +clap = { version = "4.4", features = ["derive"] } +zeroize = "1" +dotenv = "0.15" +base64 = "0.21" \ No newline at end of file diff --git a/tools/relay-server/src/config.rs b/tools/relay-server/src/config.rs new file mode 100644 index 000000000..793f42934 --- /dev/null +++ b/tools/relay-server/src/config.rs @@ -0,0 +1,47 @@ +use std::{error::Error, path::Path}; + +use base64::{ + alphabet::STANDARD, + engine::{general_purpose::PAD, GeneralPurpose}, + Engine, +}; +use rust_ipfs::{Keypair, PeerId}; +use serde::Deserialize; +use zeroize::Zeroizing; + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct IpfsConfig { + pub identity: Identity, +} + +impl IpfsConfig { + pub fn load>(path: P) -> Result> { + let file = std::fs::File::open(path)?; + let config = serde_json::from_reader(file)?; + Ok(config) + } +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Identity { + #[serde(rename = "PeerID")] + pub peer_id: PeerId, + pub priv_key: String, +} + +impl Identity { + pub fn keypair(&self) -> Result> { + let engine = GeneralPurpose::new(&STANDARD, PAD); + let keypair_bytes = Zeroizing::new(engine.decode(self.priv_key.as_bytes())?); + let keypair = Keypair::from_protobuf_encoding(&keypair_bytes)?; + Ok(keypair) + } +} + +impl zeroize::Zeroize for IpfsConfig { + fn zeroize(&mut self) { + self.identity.priv_key.zeroize(); + } +} diff --git a/tools/relay-server/src/main.rs b/tools/relay-server/src/main.rs new file mode 100644 index 000000000..24f45a1a8 --- /dev/null +++ b/tools/relay-server/src/main.rs @@ -0,0 +1,235 @@ +mod config; + +use std::{path::PathBuf, time::Duration}; + +use base64::{ + alphabet::STANDARD, + engine::{general_purpose::PAD, GeneralPurpose}, + Engine, +}; +use clap::Parser; +use rust_ipfs::{ + p2p::{RateLimit, RelayConfig, TransportConfig}, + FDLimit, Keypair, Multiaddr, UninitializedIpfsNoop as UninitializedIpfs, +}; + +use zeroize::Zeroizing; + +use crate::config::IpfsConfig; + +fn decode_kp(kp: &str) -> anyhow::Result { + let engine = GeneralPurpose::new(&STANDARD, PAD); + let keypair_bytes = Zeroizing::new(engine.decode(kp.as_bytes())?); + let keypair = Keypair::from_protobuf_encoding(&keypair_bytes)?; + Ok(keypair) +} + +fn encode_kp(kp: &Keypair) -> anyhow::Result { + let bytes = kp.to_protobuf_encoding()?; + let engine = GeneralPurpose::new(&STANDARD, PAD); + let kp_encoded = engine.encode(bytes); + Ok(kp_encoded) +} + +#[derive(Debug, Parser)] +#[clap(name = "relay-server")] +struct Opt { + /// Listening addresses in multiaddr format. If empty, will listen on all addresses available + #[clap(long)] + listen_addr: Vec, + + #[clap(long)] + keyfile: Option, + + /// Path to the ipfs instance + #[clap(long)] + path: Option, + + /// Path to ipfs config to use existing keypair + #[clap(long)] + ipfs_config: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let opts = Opt::parse(); + + let path = opts.path; + + if let Some(path) = path.as_ref() { + tokio::fs::create_dir_all(path).await?; + } + + let keypair = match opts + .keyfile + .map(|kp| path.as_ref().map(|p| p.join(kp.clone())).unwrap_or(kp)) + { + Some(kp) => match kp.is_file() { + true => { + tracing::info!("Reading keypair from {}", kp.display()); + let kp_str = tokio::fs::read_to_string(&kp).await?; + decode_kp(&kp_str)? + } + false => { + tracing::info!("Generating keypair"); + let k = Keypair::generate_ed25519(); + let encoded_kp = encode_kp(&k)?; + let kp = path.as_ref().map(|p| p.join(kp.clone())).unwrap_or(kp); + tracing::info!("Saving keypair to {}", kp.display()); + tokio::fs::write(kp, &encoded_kp).await?; + k + } + }, + None => { + if let Some(config) = opts.ipfs_config { + let config = IpfsConfig::load(config)?; + config.identity.keypair()? + } else { + tracing::info!("Generating keypair"); + Keypair::generate_ed25519() + } + } + }; + + let local_peer_id = keypair.public().to_peer_id(); + println!("Local PeerID: {local_peer_id}"); + + let mut uninitialized = UninitializedIpfs::new() + .with_ping(None) + .with_relay_server(Some(RelayConfig { + max_circuits: 512, + max_circuits_per_peer: 512, + max_circuit_duration: Duration::from_secs(2 * 60), + max_circuit_bytes: 8 * 1024 * 1024, + circuit_src_rate_limiters: vec![ + RateLimit::PerIp { + limit: 256.try_into().expect("Greater than 0"), + interval: Duration::from_secs(60 * 2), + }, + RateLimit::PerPeer { + limit: 256.try_into().expect("Greater than 0"), + interval: Duration::from_secs(60), + }, + ], + max_reservations_per_peer: 512, + max_reservations: 1024, + reservation_duration: Duration::from_secs(60 * 60), + reservation_rate_limiters: vec![ + RateLimit::PerIp { + limit: 256.try_into().expect("Greater than 0"), + interval: Duration::from_secs(60), + }, + RateLimit::PerPeer { + limit: 256.try_into().expect("Greater than 0"), + interval: Duration::from_secs(60), + }, + ], + })) + .fd_limit(FDLimit::Max) + .set_keypair(keypair) + .set_idle_connection_timeout(30) + .set_transport_configuration(TransportConfig { + enable_quic: true, + ..Default::default() + }) + .listen_as_external_addr(); + + let addrs = match opts.listen_addr.as_slice() { + [] => vec![ + "/ip4/0.0.0.0/tcp/0".parse().unwrap(), + "/ip4/0.0.0.0/udp/0/quic-v1".parse().unwrap(), + ], + addrs => addrs.to_vec(), + }; + + if let Some(path) = path { + uninitialized = uninitialized.set_path(path); + } + + uninitialized = uninitialized.set_listening_addrs(addrs); + + let _ipfs = uninitialized.start().await?; + + tokio::signal::ctrl_c().await?; + + Ok(()) +} + +mod ext_behaviour { + use std::task::{Context, Poll}; + + use rust_ipfs::libp2p::{ + core::Endpoint, + swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NewListenAddr, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, + }, + Multiaddr, PeerId, + }; + use rust_ipfs::NetworkBehaviour; + + #[derive(Default, Debug)] + pub struct Behaviour; + + impl NetworkBehaviour for Behaviour { + type ConnectionHandler = rust_ipfs::libp2p::swarm::dummy::ConnectionHandler; + type ToSwarm = void::Void; + + fn handle_pending_inbound_connection( + &mut self, + _: ConnectionId, + _: &Multiaddr, + _: &Multiaddr, + ) -> Result<(), ConnectionDenied> { + Ok(()) + } + + fn handle_pending_outbound_connection( + &mut self, + _: ConnectionId, + _: Option, + _: &[Multiaddr], + _: Endpoint, + ) -> Result, ConnectionDenied> { + Ok(vec![]) + } + + fn handle_established_inbound_connection( + &mut self, + _: ConnectionId, + _: PeerId, + _: &Multiaddr, + _: &Multiaddr, + ) -> Result, ConnectionDenied> { + Ok(rust_ipfs::libp2p::swarm::dummy::ConnectionHandler) + } + + fn handle_established_outbound_connection( + &mut self, + _: ConnectionId, + _: PeerId, + _: &Multiaddr, + _: Endpoint, + ) -> Result, ConnectionDenied> { + Ok(rust_ipfs::libp2p::swarm::dummy::ConnectionHandler) + } + + fn on_connection_handler_event( + &mut self, + _: PeerId, + _: ConnectionId, + _: THandlerOutEvent, + ) { + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + if let FromSwarm::NewListenAddr(NewListenAddr { addr, .. }) = event { + println!("Listening on {addr}"); + } + } + + fn poll(&mut self, _: &mut Context) -> Poll>> { + Poll::Pending + } + } +}