I2P Support

Co-authored-by: ps
This commit is contained in:
Lorenz Leutgeb 2026-02-15 02:13:04 +01:00 committed by Fintan Halpenny
parent 9b9b5ca996
commit ac3eba09a1
15 changed files with 168 additions and 41 deletions

View File

@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
value are asked to keep the substring `/radicle:{YOUR_VERSION}/` which allows value are asked to keep the substring `/radicle:{YOUR_VERSION}/` which allows
for better telemetry regarding version distribution on the network. for better telemetry regarding version distribution on the network.
To opt-out of sending any meaningful user agent, set `node.userAgent = null`. To opt-out of sending any meaningful user agent, set `node.userAgent = null`.
- In addition to connections via SOCKS proxy and Tor for `*.onion` names, now
connections via SOCKS proxy and I2P for `*.i2p{,.alt}` names is now supported.
To enable making connections via I2P, configure `node.i2p`.
## 1.8.0 ## 1.8.0

View File

@ -14,7 +14,8 @@ name = "rad"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
default = ["tor"] default = ["i2p", "tor"]
i2p = ["radicle/i2p"]
tor = ["radicle/tor"] tor = ["radicle/tor"]
[dependencies] [dependencies]

View File

@ -111,12 +111,13 @@ $ rad config schema
"type": "string" "type": "string"
}, },
"ConnectAddress": { "ConnectAddress": {
"description": "A node address to connect to. Format: An Ed25519 public key in multibase encoding, followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', followed by a TCP port number.", "description": "A node address to connect to. Format: An Ed25519 public key in multibase encoding, followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion name, or an I2P address, followed by the symbol ':', followed by a TCP port number.",
"type": "string", "type": "string",
"pattern": "^.+@.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$", "pattern": "^.+@.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$",
"examples": [ "examples": [
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776", "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776",
"z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"z6Mkvky2mnSYCTUMKRdAUoZXBXLLKtnWEkWeYQcGjjnmobAU@f2atcc7udeub5kh4nkljtjwyk7ikjviorufzgwnfwhkphljl3vhq.b32.i2p:8776",
"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776", "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776",
"z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337" "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337"
] ]
@ -260,6 +261,10 @@ $ rad config schema
"description": "Onion address config.", "description": "Onion address config.",
"$ref": "#/$defs/AddressConfig" "$ref": "#/$defs/AddressConfig"
}, },
"i2p": {
"description": "I2P address config.",
"$ref": "#/$defs/AddressConfig"
},
"network": { "network": {
"description": "Peer-to-peer network.", "description": "Peer-to-peer network.",
"$ref": "#/$defs/Network", "$ref": "#/$defs/Network",
@ -383,10 +388,11 @@ $ rad config schema
] ]
}, },
"Address": { "Address": {
"description": "An IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', followed by a TCP port number.", "description": "An IP address, or a DNS name, or a Tor onion name, or an I2P address,followed by the symbol ':', followed by a TCP port number.",
"type": "string", "type": "string",
"examples": [ "examples": [
"xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", "xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"f2atcc7udeub5kh4nkljtjwyk7ikjviorufzgwnfwhkphljl3vhq.b32.i2p:8776",
"seed.example.com:8776", "seed.example.com:8776",
"192.0.2.0:31337" "192.0.2.0:31337"
], ],

View File

@ -10,7 +10,8 @@ build = "build.rs"
rust-version.workspace = true rust-version.workspace = true
[features] [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"] systemd = ["dep:radicle-systemd"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "radicle-protocol/test", "qcheck", "snapbox"] test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "radicle-protocol/test", "qcheck", "snapbox"]
tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"] tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"]

View File

@ -21,7 +21,7 @@ use radicle::collections::{RandomMap, RandomSet};
use radicle::crypto; use radicle::crypto;
use radicle::node::Link; use radicle::node::Link;
use radicle::node::NodeId; use radicle::node::NodeId;
#[cfg(feature = "tor")] #[cfg(any(feature = "i2p", feature = "tor"))]
use radicle::node::config::AddressConfig; use radicle::node::config::AddressConfig;
use radicle::storage::WriteStorage; use radicle::storage::WriteStorage;
use radicle_protocol::deserializer::Deserializer; use radicle_protocol::deserializer::Deserializer;
@ -1083,6 +1083,30 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
signer: G, signer: G,
config: &radicle::node::Config, config: &radicle::node::Config,
) -> io::Result<WireSession<G>> { ) -> io::Result<WireSession<G>> {
#[cfg(any(feature = "i2p", feature = "tor"))]
fn proxy_or_forward<H: std::fmt::Display>(
config: &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.
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.
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.
AddressConfig::Drop => 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 // Determine what address to establish a TCP connection with, given the remote peer
// address and our node configuration. // address and our node configuration.
let inet_addr: NetAddr<InetHost> = match (&remote_addr.host, config.proxy) { let inet_addr: NetAddr<InetHost> = match (&remote_addr.host, config.proxy) {
@ -1093,27 +1117,11 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
(HostName::Dns(dns), None) => NetAddr::new(InetHost::Dns(dns.clone()), remote_addr.port), (HostName::Dns(dns), None) => NetAddr::new(InetHost::Dns(dns.clone()), remote_addr.port),
// For onion addresses, handle with care. // For onion addresses, handle with care.
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
(HostName::Tor(onion), proxy) => match config.onion { (HostName::Tor(onion), proxy) => {
// In onion proxy mode, simply use the configured proxy address. proxy_or_forward(&config.onion, proxy, onion, remote_addr.port)?
// This takes precedence over any global proxy. }
AddressConfig::Proxy { address } => address.into(), #[cfg(feature = "i2p")]
// In "forward" mode, if a global proxy is set, we use that, otherwise (HostName::I2p(i2p), proxy) => proxy_or_forward(&config.i2p, proxy, i2p, remote_addr.port)?,
// we treat `.onion` addresses as regular DNS names.
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.
AddressConfig::Drop => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"no configuration found for .onion addresses",
));
}
},
_ => { _ => {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::Unsupported, io::ErrorKind::Unsupported,

View File

@ -9,6 +9,7 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[features] [features]
i2p = ["cypheraddr/i2p", "radicle/i2p"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck"] test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck"]
tor = ["cypheraddr/tor", "radicle/tor"] tor = ["cypheraddr/tor", "radicle/tor"]
@ -35,4 +36,4 @@ pastey = "0.2"
qcheck = { workspace = true } qcheck = { workspace = true }
qcheck-macros = { workspace = true } qcheck-macros = { workspace = true }
radicle = { workspace = true, features = ["test"] } radicle = { workspace = true, features = ["test"] }
radicle-crypto = { workspace = true, features = ["test", "cyphernet"] } radicle-crypto = { workspace = true, features = ["test", "cyphernet"] }

View File

@ -28,8 +28,6 @@ use radicle::node;
use radicle::node::address; use radicle::node::address;
use radicle::node::address::Store as _; use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, AddressType, KnownAddress}; use radicle::node::address::{AddressBook, AddressType, KnownAddress};
#[cfg(feature = "tor")]
use radicle::node::config::AddressConfig;
use radicle::node::config::{PeerConfig, RateLimit}; use radicle::node::config::{PeerConfig, RateLimit};
use radicle::node::device::Device; use radicle::node::device::Device;
use radicle::node::refs::Store as _; use radicle::node::refs::Store as _;
@ -2652,11 +2650,18 @@ where
/// ///
/// If the [`Address`] is an `.onion` address and the service supports onion /// If the [`Address`] is an `.onion` address and the service supports onion
/// routing then this will return `true`. /// routing then this will return `true`.
///
/// # I2P
///
/// If the [`Address`] is an I2P address and the service supports I2P
/// connections then this will return `true`.
fn is_supported_address(&self, address: &Address) -> bool { fn is_supported_address(&self, address: &Address) -> bool {
match AddressType::from(address) { match AddressType::from(address) {
// Only consider onion addresses if configured. // Only consider onion addresses if configured.
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
AddressType::Onion => self.config.onion != AddressConfig::Drop, AddressType::Onion => self.config.onion != radicle::node::config::AddressConfig::Drop,
#[cfg(feature = "i2p")]
AddressType::I2p => self.config.i2p != radicle::node::config::AddressConfig::Drop,
AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true, AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true,
} }
} }

View File

@ -14,6 +14,8 @@ use std::string::FromUtf8Error;
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
#[cfg(feature = "i2p")]
use cypheraddr::i2p;
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
use cypheraddr::tor; use cypheraddr::tor;
@ -60,6 +62,9 @@ pub enum Invalid {
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
#[error("invalid onion address: {0}")] #[error("invalid onion address: {0}")]
OnionAddr(#[from] tor::OnionAddrDecodeError), OnionAddr(#[from] tor::OnionAddrDecodeError),
#[cfg(feature = "i2p")]
#[error("invalid i2p address: {0}")]
I2pAddr(#[from] i2p::I2pAddrParseError),
#[error("invalid timestamp: {actual_millis} millis")] #[error("invalid timestamp: {actual_millis} millis")]
Timestamp { actual_millis: u64 }, Timestamp { actual_millis: u64 },
@ -266,6 +271,13 @@ impl Encode for cypheraddr::tor::OnionAddrV3 {
} }
} }
#[cfg(feature = "i2p")]
impl Encode for i2p::I2pAddr {
fn encode(&self, buf: &mut impl BufMut) {
self.to_string().encode(buf)
}
}
impl Encode for UserAgent { impl Encode for UserAgent {
fn encode(&self, buf: &mut impl BufMut) { fn encode(&self, buf: &mut impl BufMut) {
self.as_ref().encode(buf) self.as_ref().encode(buf)
@ -531,6 +543,16 @@ impl Decode for tor::OnionAddrV3 {
} }
} }
#[cfg(feature = "i2p")]
impl Decode for i2p::I2pAddr {
fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
let s = String::decode(buf)?;
let addr = i2p::I2pAddr::from_str(&s).map_err(Invalid::from)?;
Ok(addr)
}
}
impl Encode for Timestamp { impl Encode for Timestamp {
fn encode(&self, buf: &mut impl BufMut) { fn encode(&self, buf: &mut impl BufMut) {
self.deref().encode(buf) self.deref().encode(buf)

View File

@ -2,6 +2,8 @@ use std::{mem, net};
use bytes::Buf; use bytes::Buf;
use bytes::BufMut; use bytes::BufMut;
#[cfg(feature = "i2p")]
use cypheraddr::i2p;
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
use cypheraddr::tor; use cypheraddr::tor;
use cypheraddr::{HostName, NetAddr}; use cypheraddr::{HostName, NetAddr};
@ -83,6 +85,8 @@ pub enum AddressType {
Dns = 3, Dns = 3,
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
Onion = 4, Onion = 4,
#[cfg(feature = "i2p")]
I2p = 5,
} }
impl From<AddressType> for u8 { impl From<AddressType> for u8 {
@ -99,6 +103,8 @@ impl From<&Address> for AddressType {
HostName::Dns(_) => AddressType::Dns, HostName::Dns(_) => AddressType::Dns,
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
HostName::Tor(_) => AddressType::Onion, HostName::Tor(_) => AddressType::Onion,
#[cfg(feature = "i2p")]
HostName::I2p(_) => AddressType::I2p,
_ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive` _ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
} }
} }
@ -114,6 +120,8 @@ impl TryFrom<u8> for AddressType {
3 => Ok(AddressType::Dns), 3 => Ok(AddressType::Dns),
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
4 => Ok(AddressType::Onion), 4 => Ok(AddressType::Onion),
#[cfg(feature = "i2p")]
5 => Ok(AddressType::I2p),
_ => Err(other), _ => Err(other),
} }
} }
@ -366,6 +374,11 @@ impl wire::Encode for Address {
u8::from(AddressType::Onion).encode(buf); u8::from(AddressType::Onion).encode(buf);
addr.encode(buf); addr.encode(buf);
} }
#[cfg(feature = "i2p")]
HostName::I2p(ref addr) => {
u8::from(AddressType::I2p).encode(buf);
addr.encode(buf);
}
_ => { _ => {
unimplemented!( unimplemented!(
"Encoding not defined for addresses of the same type as the following: {:?}", "Encoding not defined for addresses of the same type as the following: {:?}",
@ -405,6 +418,12 @@ impl wire::Decode for Address {
HostName::Tor(onion) HostName::Tor(onion)
} }
#[cfg(feature = "i2p")]
Ok(AddressType::I2p) => {
let i2p: i2p::I2pAddr = wire::Decode::decode(buf)?;
HostName::I2p(i2p)
}
Err(other) => return Err(wire::Invalid::AddressType { actual: other }.into()), Err(other) => return Err(wire::Invalid::AddressType { actual: other }.into()),
}; };
let port = u16::decode(buf)?; let port = u16::decode(buf)?;

View File

@ -13,6 +13,7 @@ rust-version.workspace = true
bstr = ["radicle-git-ref-format/bstr"] bstr = ["radicle-git-ref-format/bstr"]
default = [] default = []
gix = ["radicle-oid/gix"] gix = ["radicle-oid/gix"]
i2p = ["cyphernet/i2p"]
test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"] test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"] logger = ["colored", "chrono"]
qcheck = [ qcheck = [

View File

@ -468,11 +468,11 @@ impl TryFrom<&sqlite::Value> for Alias {
feature = "schemars", feature = "schemars",
derive(schemars::JsonSchema), derive(schemars::JsonSchema),
schemars(description = "\ schemars(description = "\
An IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', \ An IP address, or a DNS name, or a Tor onion name, or an I2P address,\
followed by a TCP port number.", followed by the symbol ':', followed by a TCP port number.",
extend( extend("examples" = [
"examples" = [
"xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", "xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"f2atcc7udeub5kh4nkljtjwyk7ikjviorufzgwnfwhkphljl3vhq.b32.i2p:8776",
"seed.example.com:8776", "seed.example.com:8776",
"192.0.2.0:31337", "192.0.2.0:31337",
], ],
@ -517,6 +517,12 @@ impl Address {
matches!(self.0.host, HostName::Tor(_)) matches!(self.0.host, HostName::Tor(_))
} }
/// Returns `true` if the [`HostName`] is an I2P address.
#[cfg(feature = "i2p")]
pub fn is_i2p(&self) -> bool {
matches!(self.0.host, HostName::I2p(_))
}
/// Return the port number of the [`Address`]. /// Return the port number of the [`Address`].
pub fn port(&self) -> u16 { pub fn port(&self) -> u16 {
self.0.port self.0.port
@ -537,6 +543,8 @@ impl Address {
.collect::<String>(); .collect::<String>();
format!("{start}{end}") format!("{start}{end}")
} }
#[cfg(feature = "i2p")]
HostName::I2p(i2p) => i2p.to_string(),
_ => unreachable!(), _ => unreachable!(),
}; };

View File

@ -203,6 +203,8 @@ pub enum AddressType {
Dns = 3, Dns = 3,
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
Onion = 4, Onion = 4,
#[cfg(feature = "i2p")]
I2p = 5,
} }
impl From<AddressType> for u8 { impl From<AddressType> for u8 {
@ -219,6 +221,8 @@ impl From<&Address> for AddressType {
HostName::Dns(_) => AddressType::Dns, HostName::Dns(_) => AddressType::Dns,
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
HostName::Tor(_) => AddressType::Onion, HostName::Tor(_) => AddressType::Onion,
#[cfg(feature = "i2p")]
HostName::I2p(_) => AddressType::I2p,
_ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive` _ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
} }
} }
@ -234,6 +238,8 @@ impl TryFrom<u8> for AddressType {
3 => Ok(AddressType::Dns), 3 => Ok(AddressType::Dns),
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
4 => Ok(AddressType::Onion), 4 => Ok(AddressType::Onion),
#[cfg(feature = "i2p")]
5 => Ok(AddressType::I2p),
_ => Err(other), _ => Err(other),
} }
} }

View File

@ -537,6 +537,8 @@ impl TryFrom<&sql::Value> for AddressType {
"dns" => Ok(AddressType::Dns), "dns" => Ok(AddressType::Dns),
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
"onion" => Ok(AddressType::Onion), "onion" => Ok(AddressType::Onion),
#[cfg(feature = "i2p")]
"i2p" => Ok(AddressType::I2p),
_ => Err(err), _ => Err(err),
}, },
_ => Err(err), _ => Err(err),
@ -552,6 +554,8 @@ impl sql::BindableWithIndex for AddressType {
Self::Dns => "dns".bind(stmt, i), Self::Dns => "dns".bind(stmt, i),
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
Self::Onion => "onion".bind(stmt, i), Self::Onion => "onion".bind(stmt, i),
#[cfg(feature = "i2p")]
Self::I2p => "i2p".bind(stmt, i),
} }
} }
} }

View File

@ -278,7 +278,7 @@ pub struct RateLimits {
schemars(description = "\ schemars(description = "\
A node address to connect to. Format: An Ed25519 public key in multibase encoding, \ A node address to connect to. Format: An Ed25519 public key in multibase encoding, \
followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion \ followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion \
name, followed by the symbol ':', followed by a TCP port number.\ name, or an I2P address, followed by the symbol ':', followed by a TCP port number.\
") ")
)] )]
pub struct ConnectAddress( pub struct ConnectAddress(
@ -289,6 +289,7 @@ pub struct ConnectAddress(
extend("examples" = [ extend("examples" = [
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776", "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776",
"z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"z6Mkvky2mnSYCTUMKRdAUoZXBXLLKtnWEkWeYQcGjjnmobAU@f2atcc7udeub5kh4nkljtjwyk7ikjviorufzgwnfwhkphljl3vhq.b32.i2p:8776",
"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776", "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776",
"z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337", "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337",
]), ]),
@ -358,7 +359,7 @@ pub enum Relay {
#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")] #[serde(rename_all = "camelCase", tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg(feature = "tor")] #[cfg(any(feature = "i2p", feature = "tor"))]
pub enum AddressConfig { pub enum AddressConfig {
/// Proxy connections to this address type. /// Proxy connections to this address type.
Proxy { Proxy {
@ -561,6 +562,10 @@ pub struct Config {
deserialize_with = "crate::serde_ext::null_to_default" deserialize_with = "crate::serde_ext::null_to_default"
)] )]
pub onion: AddressConfig, pub onion: AddressConfig,
/// I2P address config.
#[cfg(feature = "i2p")]
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
pub i2p: AddressConfig,
/// Peer-to-peer network. /// Peer-to-peer network.
#[serde(default)] #[serde(default)]
pub network: Network, pub network: Network,
@ -618,6 +623,8 @@ impl Config {
proxy: None, proxy: None,
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
onion: AddressConfig::Drop, onion: AddressConfig::Drop,
#[cfg(feature = "i2p")]
i2p: AddressConfig::Drop,
relay: Relay::default(), relay: Relay::default(),
limits: Limits::default(), limits: Limits::default(),
workers: Workers::default(), workers: Workers::default(),

View File

@ -5,6 +5,8 @@ use std::str::FromStr;
use std::{iter, net}; use std::{iter, net};
use crypto::PublicKey; use crypto::PublicKey;
#[cfg(feature = "i2p")]
use cyphernet::addr::i2p::I2pAddr;
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
use cyphernet::{EcPk, addr::tor::OnionAddrV3}; use cyphernet::{EcPk, addr::tor::OnionAddrV3};
use qcheck::Arbitrary; use qcheck::Arbitrary;
@ -208,10 +210,14 @@ impl Arbitrary for MockRepository {
impl Arbitrary for AddressType { impl Arbitrary for AddressType {
fn arbitrary(g: &mut qcheck::Gen) -> Self { fn arbitrary(g: &mut qcheck::Gen) -> Self {
#[cfg(not(feature = "tor"))] #[allow(unused_mut)]
let types = [1, 2, 3]; let mut types = vec![1, 2, 3];
#[cfg(feature = "tor")] #[cfg(feature = "tor")]
let types = [1, 2, 3, 4]; types.push(4);
#[cfg(feature = "i2p")]
types.push(5);
let t = *g.choose(&types).unwrap() as u8; let t = *g.choose(&types).unwrap() as u8;
@ -238,11 +244,40 @@ impl Arbitrary for Address {
AddressType::Onion => { AddressType::Onion => {
let pk = PublicKey::arbitrary(g); let pk = PublicKey::arbitrary(g);
let addr = OnionAddrV3::from( let addr = OnionAddrV3::from(
cyphernet::ed25519::PublicKey::from_pk_compressed(pk.to_byte_array()) cyphernet::ed25519::PublicKey::from_pk_compressed(pk.to_byte_array()).unwrap(),
.unwrap(),
); );
cyphernet::addr::HostName::Tor(addr) cyphernet::addr::HostName::Tor(addr)
} }
#[cfg(feature = "i2p")]
AddressType::I2p => {
let address = if bool::arbitrary(g) {
let name: String = iter::repeat_with(|| {
char::from(
// Base32 alphabet from RFC 4648.
*g.choose(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
.expect("alphabet is non-empty"),
)
})
.take(56)
.collect();
name + ".b32"
} else {
g.choose(&["iris.radicle.example", "rosa.radicle.example"])
.unwrap()
.to_string()
};
let suffix = if bool::arbitrary(g) {
".i2p"
} else {
".i2p.alt"
};
let address = address + suffix;
cyphernet::addr::HostName::I2p(I2pAddr::from_str(&address).unwrap())
}
}; };
Address::from(cyphernet::addr::NetAddr { Address::from(cyphernet::addr::NetAddr {