Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add shuttle hotspot #650

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"extensions/*",
"warp",
"tools/*",
"extensions/warp-ipfs/hotspot",
"extensions/warp-ipfs/shuttle",
"extensions/warp-ipfs/examples/wasm-ipfs-identity",
"extensions/warp-ipfs/examples/wasm-ipfs-friends",
Expand Down
4 changes: 4 additions & 0 deletions extensions/warp-ipfs/examples/identity-interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct Opt {
import: Option<PathBuf>,
#[clap(long)]
phrase: Option<String>,
#[clap(long)]
bootstrap_preload: Vec<Multiaddr>,
}

async fn account(
Expand Down Expand Up @@ -93,6 +95,8 @@ async fn account(
*config.enable_relay_mut() = false;
}

config.ipfs_setting_mut().preload = opt.bootstrap_preload.clone();

if opt.upnp {
config.ipfs_setting_mut().portmapping = true;
}
Expand Down
19 changes: 19 additions & 0 deletions extensions/warp-ipfs/hotspot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "hotspot"
version = "0.1.0"
edition = "2021"

[dependencies]
warp-ipfs = { path = "../" }
rust-ipfs = { workspace = true, features = ["webrtc_transport", "experimental_stream"] }
anyhow.workspace = true
serde.workspace = true
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

clap = { version = "4.4", features = ["derive"] }
zeroize.workspace = true
base64 = "0.22"

tokio = { workspace = true }
toml = "0.8.19"
254 changes: 254 additions & 0 deletions extensions/warp-ipfs/hotspot/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use base64::alphabet::STANDARD;
use base64::engine::general_purpose::PAD;
use base64::engine::GeneralPurpose;
use base64::Engine;
use clap::Parser;
use rust_ipfs::p2p::{RateLimit, RelayConfig};
use rust_ipfs::{Keypair, Multiaddr};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::{num::NonZeroU32, path::PathBuf, time::Duration};
use warp_ipfs::hotspot;
use zeroize::Zeroizing;

fn decode_kp(kp: &str) -> anyhow::Result<Keypair> {
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<String> {
let bytes = kp.to_protobuf_encoding()?;
let engine = GeneralPurpose::new(&STANDARD, PAD);
let kp_encoded = engine.encode(bytes);
Ok(kp_encoded)
}

#[derive(Clone, Deserialize, Serialize)]
struct Config {
pub max_circuits: Option<usize>,
pub max_circuits_per_peer: Option<usize>,
pub max_circuit_duration: Option<Duration>,
pub max_circuit_bytes: Option<u64>,
pub circuit_rate_limiters: Option<Vec<Rate>>,
pub max_reservations_per_peer: Option<usize>,
pub max_reservations: Option<usize>,
pub reservation_duration: Option<Duration>,
pub reservation_rate_limiters: Option<Vec<Rate>>,
}

#[derive(Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Rate {
PerPeer {
limit: NonZeroU32,
interval: Duration,
},
PerIp {
limit: NonZeroU32,
interval: Duration,
},
}

impl From<Rate> for RateLimit {
fn from(rate: Rate) -> Self {
match rate {
Rate::PerPeer { limit, interval } => RateLimit::PerPeer { limit, interval },
Rate::PerIp { limit, interval } => RateLimit::PerIp { limit, interval },
}
}
}

impl Default for Config {
fn default() -> Self {
Self {
max_circuits: Some(32768),
max_circuits_per_peer: Some(32768),
max_circuit_duration: Some(Duration::from_secs(60 * 2)),
max_circuit_bytes: Some(512 * 1024 * 1024),
circuit_rate_limiters: Some(vec![
Rate::PerPeer {
limit: 32768.try_into().expect("greater than zero"),
interval: Duration::from_secs(60 * 2),
},
Rate::PerIp {
limit: 32768.try_into().expect("greater than zero"),
interval: Duration::from_secs(30),
},
]),
max_reservations_per_peer: Some(32768),
max_reservations: Some(32768),
reservation_duration: Some(Duration::from_secs(60 * 60)),
reservation_rate_limiters: Some(vec![
Rate::PerPeer {
limit: 32768.try_into().expect("greater than zero"),
interval: Duration::from_secs(30),
},
Rate::PerIp {
limit: 32768.try_into().expect("greater than zero"),
interval: Duration::from_secs(30),
},
]),
}
}
}

impl From<Config> for RelayConfig {
fn from(config: Config) -> Self {
let mut circuit_src_rate_limiters = vec![];
let circuit_rate = config.circuit_rate_limiters.unwrap_or_default();
circuit_src_rate_limiters.extend(circuit_rate.iter().map(|s| (*s).into()));

let mut reservation_rate_limiters = vec![];
let reservation_rate = config.reservation_rate_limiters.unwrap_or_default();
reservation_rate_limiters.extend(reservation_rate.iter().map(|s| (*s).into()));

RelayConfig {
max_circuits: config.max_circuits.unwrap_or(32768),
max_circuits_per_peer: config.max_circuits_per_peer.unwrap_or(32768),
max_circuit_duration: config
.max_circuit_duration
.unwrap_or(Duration::from_secs(2 * 60)),
max_circuit_bytes: config.max_circuit_bytes.unwrap_or(512 * 1024 * 1024),
circuit_src_rate_limiters,
max_reservations_per_peer: config.max_reservations_per_peer.unwrap_or(21768),
max_reservations: config.max_reservations.unwrap_or(32768),
reservation_duration: config
.reservation_duration
.unwrap_or(Duration::from_secs(60 * 60)),
reservation_rate_limiters,
}
}
}

#[derive(Debug, Parser)]
#[clap(name = "shuttle-hotspot")]
struct Opt {
/// Listening addresses in multiaddr format. If empty, will listen on all addresses available
#[clap(long)]
listen_addr: Vec<Multiaddr>,

/// External address in multiaddr format that would indicate how the node can be reached.
/// If empty, all listening addresses will be used as an external address
#[clap(long)]
external_addr: Vec<Multiaddr>,

/// Path to key file
#[clap(long)]
keyfile: Option<PathBuf>,

/// Path to a configuration file to adjust relay setting
#[clap(long)]
relay_config: Option<PathBuf>,

/// Enables secured websockets
#[clap(long)]
enable_wss: bool,

/// Enables webrtc
#[clap(long)]
enable_webrtc: bool,

/// Use unbounded configuration with higher limits
#[clap(long)]
unbounded: bool,

/// TLS Certificate when websocket is used
/// Note: websocket required a signed certificate.
#[clap(long)]
ws_tls_certificate: Option<Vec<PathBuf>>,

/// TLS Private Key when websocket is used
#[clap(long)]
ws_tls_private_key: Option<PathBuf>,
}

#[cfg(not(target_arch = "wasm32"))]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();

let opts = Opt::parse();

let keypair = match opts.keyfile {
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)?;
tracing::info!("Saving keypair to {}", kp.display());
tokio::fs::write(kp, &encoded_kp).await?;
k
}
},
None => {
tracing::info!("Generating keypair");
Keypair::generate_ed25519()
}
};

let config = match opts.relay_config {
Some(path) => match path.is_file() {
true => {
let conf = tokio::fs::read_to_string(path).await?;
let config: Config = toml::from_str(&conf)?;
config
}
false => {
let config = Config::default();
let bytes = toml::to_string(&config)?;
tokio::fs::write(path, &bytes).await?;
config
}
},
None => Config::default(),
};

let (ws_cert, ws_pk) = match (opts.ws_tls_certificate, opts.ws_tls_private_key) {
(Some(cert), Some(prv)) => {
let mut certs = Vec::with_capacity(cert.len());
for c in cert {
let Ok(cert) = tokio::fs::read_to_string(c).await else {
continue;
};
certs.push(cert);
}

let prv = tokio::fs::read_to_string(prv).await.ok();
((!certs.is_empty()).then_some(certs), prv)
}
_ => (None, None),
};

let wss_opt = ws_cert.and_then(|list| ws_pk.map(|k| (list, k)));

let local_peer_id = keypair.public().to_peer_id();
println!("Local PeerID: {local_peer_id}");

let _handle = hotspot::Hotspot::new(
&keypair,
opts.enable_wss,
wss_opt,
opts.enable_webrtc,
true,
false,
&opts.listen_addr,
&opts.external_addr,
Some(config.into()),
true,
)
.await?;

tokio::signal::ctrl_c().await?;

Ok(())
}

#[cfg(target_arch = "wasm32")]
fn main() {}
29 changes: 12 additions & 17 deletions extensions/warp-ipfs/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,24 @@ impl Default for RelayClient {
#[cfg(not(target_arch = "wasm32"))]
relay_address: vec![
//NYC-1
"/ip4/146.190.184.59/tcp/4001/p2p/12D3KooWCHWLQXTR2N6ukWM99pZYc4TM82VS7eVaDE4Ryk8ked8h".parse().unwrap(),
"/ip4/146.190.184.59/tcp/4001/p2p/12D3KooWCHWLQXTR2N6ukWM99pZYc4TM82VS7eVaDE4Ryk8ked8h".parse().unwrap(),
"/ip4/146.190.184.59/udp/4001/quic-v1/p2p/12D3KooWCHWLQXTR2N6ukWM99pZYc4TM82VS7eVaDE4Ryk8ked8h".parse().unwrap(),
//SF-1
"/ip4/64.225.88.100/udp/4001/quic-v1/p2p/12D3KooWMfyuTCbehQYy68zPH6vpGUwg8raKbrS7pd3qZrG7bFuB".parse().unwrap(),
"/ip4/64.225.88.100/tcp/4001/p2p/12D3KooWMfyuTCbehQYy68zPH6vpGUwg8raKbrS7pd3qZrG7bFuB".parse().unwrap(),
"/ip4/64.225.88.100/udp/4001/quic-v1/p2p/12D3KooWMfyuTCbehQYy68zPH6vpGUwg8raKbrS7pd3qZrG7bFuB".parse().unwrap(),
"/ip4/64.225.88.100/tcp/4001/p2p/12D3KooWMfyuTCbehQYy68zPH6vpGUwg8raKbrS7pd3qZrG7bFuB".parse().unwrap(),
//NYC-1-EXP
"/ip4/24.199.86.91/udp/46315/quic-v1/p2p/12D3KooWQcyxuNXxpiM7xyoXRZC7Vhfbh2yCtRg272CerbpFkhE6".parse().unwrap(),
"/ip4/24.199.86.91/tcp/46315/p2p/12D3KooWQcyxuNXxpiM7xyoXRZC7Vhfbh2yCtRg272CerbpFkhE6".parse().unwrap()
],
// Relays that are meant to be used from a web standpoint.
// Note: webrtc addresses are prone to change due an upstream issue and shouldnt be relied on for primary connections
#[cfg(target_arch="wasm32")]
#[cfg(target_arch = "wasm32")]
relay_address: vec![
//NYC-1
"/dns4/nyc-3-dev.relay.satellite.im/tcp/4410/wss/p2p/12D3KooWJWw4KG2KKpUxQAc8kZZDqmownRvjWGxnr5Y6XRur8WSx".parse().unwrap(),
],
background: true,
quorum: Default::default()
quorum: Default::default(),
}
}
}
Expand All @@ -107,6 +107,7 @@ pub struct IpfsSetting {
/// Used for testing with a memory transport
pub memory_transport: bool,
pub dht_client: bool,
pub preload: Vec<Multiaddr>,
}

pub type DefaultPfpFn = std::sync::Arc<
Expand All @@ -116,14 +117,8 @@ pub type DefaultPfpFn = std::sync::Arc<
#[derive(Clone)]
pub struct StoreSetting {
/// Allow only interactions with friends
/// Note: This is ignored when it comes to chating between group chat recipients
/// Note: This is ignored when it comes to chatting between group chat recipients
pub with_friends: bool,
/// Interval for broadcasting out identity (cannot be less than 3 minutes)
/// Note:
/// - If `None`, this will be disabled
/// - Will default to 3 minutes if less than
/// - This may be removed in the future
pub auto_push: Option<Duration>,
/// Discovery type
pub discovery: Discovery,

Expand All @@ -133,10 +128,11 @@ pub struct StoreSetting {
pub friend_request_response_duration: Option<Duration>,
/// Disable providing images for identities
pub disable_images: bool,
/// Announce to mesh network
pub announce_to_mesh: bool,
/// Function to call to provide data for a default profile picture if one is not apart of the identity
/// Function to call to provide data for a default profile picture if one is not a part of the identity
pub default_profile_picture: Option<DefaultPfpFn>,
/// Duration in seconds before the identity document is announced
/// Note: This field should be left as default unless it is used for testing
pub auto_push_duration: Duration,
}

impl std::fmt::Debug for StoreSetting {
Expand All @@ -148,7 +144,6 @@ impl std::fmt::Debug for StoreSetting {
impl Default for StoreSetting {
fn default() -> Self {
Self {
auto_push: None,
discovery: Discovery::Namespace {
namespace: None,
discovery_type: Default::default(),
Expand All @@ -158,7 +153,7 @@ impl Default for StoreSetting {
disable_images: false,
with_friends: false,
default_profile_picture: None,
announce_to_mesh: false,
auto_push_duration: Duration::from_secs(60),
}
}
}
Expand Down
Loading
Loading