Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
I2P Support
✗ CI failure Lorenz Leutgeb committed 2 months ago
commit f74bb2262ab9028c5c094f7fbec53b81a757afa5
parent 1e90ee862042cd408908d2321e5529cdc7a29087
2 failed (2 total) View logs
13 files changed +103 -25
modified crates/radicle-cli/Cargo.toml
@@ -14,7 +14,8 @@ name = "rad"
path = "src/main.rs"

[features]
-
default = ["tor"]
+
default = ["i2p", "tor"]
+
i2p = ["radicle/i2p"]
tor = ["radicle/tor"]

[dependencies]
modified crates/radicle-cli/src/terminal/format.rs
@@ -47,6 +47,11 @@ pub fn addr_compact(address: &Address) -> Paint<String> {
                .collect::<String>();
            format!("{start}…{end}")
        }
+
        #[cfg(feature = "i2p")]
+
        HostName::I2p(i2p) => {
+
            // TODO: Base32 addresses can be shortened like onion addresses.
+
            i2p.to_string()
+
        }
        _ => unreachable!(),
    };

modified crates/radicle-node/Cargo.toml
@@ -10,7 +10,8 @@ build = "build.rs"
rust-version.workspace = true

[features]
-
default = ["backtrace", "systemd", "structured-logger", "socket2", "tor"]
+
default = ["backtrace", "i2p", "systemd", "structured-logger", "socket2", "tor"]
+
i2p = ["cyphernet/i2p", "radicle/i2p", "radicle-protocol/i2p"]
systemd = ["dep:radicle-systemd"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "radicle-protocol/test", "qcheck", "snapbox"]
tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"]
modified crates/radicle-node/src/wire.rs
@@ -19,7 +19,7 @@ use radicle::node::device::Device;

use radicle::collections::{RandomMap, RandomSet};
use radicle::crypto;
-
#[cfg(feature = "tor")]
+
#[cfg(any(feature = "tor", feature = "i2p"))]
use radicle::node::config::AddressConfig;
use radicle::node::Link;
use radicle::node::NodeId;
@@ -1084,6 +1084,30 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
    signer: G,
    config: &radicle::node::Config,
) -> io::Result<WireSession<G>> {
+
    #[cfg(any(feature = "tor", feature = "i2p"))]
+
    fn proxy_or_forward<H: std::fmt::Display>(
+
        config: Option<&AddressConfig>,
+
        global_proxy: Option<net::SocketAddr>,
+
        host: H,
+
        port: u16,
+
    ) -> io::Result<NetAddr<InetHost>> {
+
        match config {
+
            // In proxy mode, simply use the configured proxy address.
+
            // This takes precedence over any global proxy.
+
            Some(AddressConfig::Proxy { address }) => Ok((*address).into()),
+
            // In "forward" mode, if a global proxy is set, we use that, otherwise
+
            // we treat the address as a regular DNS name.
+
            Some(AddressConfig::Forward) => Ok(global_proxy
+
                .map(Into::into)
+
                .unwrap_or_else(|| NetAddr::new(InetHost::Dns(host.to_string()), port))),
+
            // If address type support isn't configured, refuse to connect.
+
            None => Err(io::Error::new(
+
                io::ErrorKind::Unsupported,
+
                "no configuration found for address type",
+
            )),
+
        }
+
    }
+

    // Determine what address to establish a TCP connection with, given the remote peer
    // address and our node configuration.
    let inet_addr: NetAddr<InetHost> = match (&remote_addr.host, config.proxy) {
@@ -1094,27 +1118,13 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
        (HostName::Dns(dns), None) => NetAddr::new(InetHost::Dns(dns.clone()), remote_addr.port),
        // For onion addresses, handle with care.
        #[cfg(feature = "tor")]
-
        (HostName::Tor(onion), proxy) => match config.onion {
-
            // In onion proxy mode, simply use the configured proxy address.
-
            // This takes precedence over any global proxy.
-
            Some(AddressConfig::Proxy { address }) => address.into(),
-
            // In "forward" mode, if a global proxy is set, we use that, otherwise
-
            // we treat `.onion` addresses as regular DNS names.
-
            Some(AddressConfig::Forward) => {
-
                if let Some(proxy) = proxy {
-
                    proxy.into()
-
                } else {
-
                    NetAddr::new(InetHost::Dns(onion.to_string()), remote_addr.port)
-
                }
-
            }
-
            // If onion address support isn't configured, refuse to connect.
-
            None => {
-
                return Err(io::Error::new(
-
                    io::ErrorKind::Unsupported,
-
                    "no configuration found for .onion addresses",
-
                ));
-
            }
-
        },
+
        (HostName::Tor(onion), proxy) => {
+
            proxy_or_forward(config.onion.as_ref(), proxy, onion, remote_addr.port)?
+
        }
+
        #[cfg(feature = "i2p")]
+
        (HostName::I2p(i2p), proxy) => {
+
            proxy_or_forward(config.i2p.as_ref(), proxy, i2p, remote_addr.port)?
+
        }
        _ => {
            return Err(io::Error::new(
                io::ErrorKind::Unsupported,
modified crates/radicle-protocol/Cargo.toml
@@ -9,6 +9,7 @@ edition.workspace = true
rust-version.workspace = true

[features]
+
i2p = ["cyphernet/i2p"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck"]
tor = ["cyphernet/tor"]

modified crates/radicle-protocol/src/service.rs
@@ -2630,6 +2630,8 @@ where
            // Only consider onion addresses if configured.
            #[cfg(feature = "tor")]
            AddressType::Onion => self.config.onion.is_some(),
+
            #[cfg(feature = "i2p")]
+
            AddressType::I2p => self.config.i2p.is_some(),
            AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true,
        }
    }
modified crates/radicle-protocol/src/wire.rs
@@ -62,6 +62,9 @@ pub enum Invalid {
    #[cfg(feature = "tor")]
    #[error("invalid onion address: {0}")]
    OnionAddr(#[from] tor::OnionAddrDecodeError),
+
    #[cfg(feature = "i2p")]
+
    #[error("invalid i2p address: {0}")]
+
    I2pAddr(#[from] cyphernet::addr::i2p::I2pAddrParseError),
    #[error("invalid timestamp: {actual_millis} millis")]
    Timestamp { actual_millis: u64 },

@@ -266,6 +269,13 @@ impl Encode for cyphernet::addr::tor::OnionAddrV3 {
    }
}

+
#[cfg(feature = "i2p")]
+
impl Encode for cyphernet::addr::i2p::I2pAddr {
+
    fn encode(&self, buf: &mut impl BufMut) {
+
        self.to_string().encode(buf)
+
    }
+
}
+

impl Encode for UserAgent {
    fn encode(&self, buf: &mut impl BufMut) {
        self.as_ref().encode(buf)
@@ -549,6 +559,16 @@ impl Decode for tor::OnionAddrV3 {
    }
}

+
#[cfg(feature = "i2p")]
+
impl Decode for cyphernet::addr::i2p::I2pAddr {
+
    fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
+
        let s = String::decode(buf)?;
+
        let addr = cyphernet::addr::i2p::I2pAddr::from_str(&s).map_err(Invalid::from)?;
+

+
        Ok(addr)
+
    }
+
}
+

impl Encode for Timestamp {
    fn encode(&self, buf: &mut impl BufMut) {
        self.deref().encode(buf)
modified crates/radicle-protocol/src/wire/message.rs
@@ -83,6 +83,8 @@ pub enum AddressType {
    Dns = 3,
    #[cfg(feature = "tor")]
    Onion = 4,
+
    #[cfg(feature = "i2p")]
+
    I2p = 5,
}

impl From<AddressType> for u8 {
@@ -99,6 +101,8 @@ impl From<&Address> for AddressType {
            HostName::Dns(_) => AddressType::Dns,
            #[cfg(feature = "tor")]
            HostName::Tor(_) => AddressType::Onion,
+
            #[cfg(feature = "i2p")]
+
            HostName::I2p(_) => AddressType::I2p,
            _ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
        }
    }
@@ -114,6 +118,8 @@ impl TryFrom<u8> for AddressType {
            3 => Ok(AddressType::Dns),
            #[cfg(feature = "tor")]
            4 => Ok(AddressType::Onion),
+
            #[cfg(feature = "i2p")]
+
            5 => Ok(AddressType::I2p),
            _ => Err(other),
        }
    }
@@ -366,6 +372,11 @@ impl wire::Encode for Address {
                u8::from(AddressType::Onion).encode(buf);
                addr.encode(buf);
            }
+
            #[cfg(feature = "i2p")]
+
            HostName::I2p(addr) => {
+
                u8::from(AddressType::I2p).encode(buf);
+
                addr.encode(buf);
+
            }
            _ => {
                unimplemented!(
                    "Encoding not defined for addresses of the same type as the following: {:?}",
@@ -405,6 +416,12 @@ impl wire::Decode for Address {

                HostName::Tor(onion)
            }
+
            #[cfg(feature = "i2p")]
+
            Ok(AddressType::I2p) => {
+
                let i2p: cyphernet::addr::i2p::I2pAddr = wire::Decode::decode(buf)?;
+

+
                HostName::I2p(i2p)
+
            }
            Err(other) => return Err(wire::Invalid::AddressType { actual: other }.into()),
        };
        let port = u16::decode(buf)?;
modified crates/radicle/Cargo.toml
@@ -11,6 +11,7 @@ rust-version.workspace = true

[features]
default = []
+
i2p = ["cyphernet/i2p"]
test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"]
qcheck = [
modified crates/radicle/src/node/address.rs
@@ -203,6 +203,8 @@ pub enum AddressType {
    Dns = 3,
    #[cfg(feature = "tor")]
    Onion = 4,
+
    #[cfg(feature = "i2p")]
+
    I2p = 5,
}

impl From<AddressType> for u8 {
@@ -219,6 +221,8 @@ impl From<&Address> for AddressType {
            HostName::Dns(_) => AddressType::Dns,
            #[cfg(feature = "tor")]
            HostName::Tor(_) => AddressType::Onion,
+
            #[cfg(feature = "i2p")]
+
            HostName::I2p(_) => AddressType::I2p,
            _ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
        }
    }
@@ -234,6 +238,8 @@ impl TryFrom<u8> for AddressType {
            3 => Ok(AddressType::Dns),
            #[cfg(feature = "tor")]
            4 => Ok(AddressType::Onion),
+
            #[cfg(feature = "i2p")]
+
            5 => Ok(AddressType::I2p),
            _ => Err(other),
        }
    }
modified crates/radicle/src/node/address/store.rs
@@ -537,6 +537,8 @@ impl TryFrom<&sql::Value> for AddressType {
                "dns" => Ok(AddressType::Dns),
                #[cfg(feature = "tor")]
                "onion" => Ok(AddressType::Onion),
+
                #[cfg(feature = "i2p")]
+
                "i2p" => Ok(AddressType::I2p),
                _ => Err(err),
            },
            _ => Err(err),
@@ -552,6 +554,8 @@ impl sql::BindableWithIndex for AddressType {
            Self::Dns => "dns".bind(stmt, i),
            #[cfg(feature = "tor")]
            Self::Onion => "onion".bind(stmt, i),
+
            #[cfg(feature = "i2p")]
+
            Self::I2p => "i2p".bind(stmt, i),
        }
    }
}
modified crates/radicle/src/node/config.rs
@@ -355,7 +355,7 @@ pub enum Relay {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-
#[cfg(feature = "tor")]
+
#[cfg(any(feature = "tor", feature = "i2p"))]
pub enum AddressConfig {
    /// Proxy connections to this address type.
    Proxy {
@@ -436,6 +436,10 @@ pub struct Config {
    #[cfg(feature = "tor")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub onion: Option<AddressConfig>,
+
    /// I2P address config.
+
    #[cfg(feature = "i2p")]
+
    #[serde(default, skip_serializing_if = "Option::is_none")]
+
    pub i2p: Option<AddressConfig>,
    /// Peer-to-peer network.
    #[serde(default)]
    pub network: Network,
@@ -486,6 +490,8 @@ impl Config {
            proxy: None,
            #[cfg(feature = "tor")]
            onion: None,
+
            #[cfg(feature = "i2p")]
+
            i2p: None,
            relay: Relay::default(),
            limits: Limits::default(),
            workers: Workers::default(),
modified crates/radicle/src/test/arbitrary.rs
@@ -301,6 +301,10 @@ impl Arbitrary for Address {
                );
                cyphernet::addr::HostName::Tor(addr)
            }
+
            #[cfg(feature = "i2p")]
+
            AddressType::I2p => {
+
                todo!()
+
            }
        };

        Address::from(cyphernet::addr::NetAddr {