use super::*;
use fips_core::config::{
PeerAdmissionConfig, PeerConfig, TransportInstances, UdpConfig, WebSocketConfig,
};
#[derive(Clone)]
pub(in crate::core) struct ProviderReadOptions {
config: fips_core::Config,
}
impl ProviderReadOptions {
pub(in crate::core) fn new(source: &fips_core::Config, public_peers: Vec<PeerConfig>) -> Self {
let mut config = fips_core::Config::new();
config.node.strict_client_only = true;
config.node.routing.mode = fips_core::config::RoutingMode::ReplyLearned;
config.node.control.enabled = false;
config.node.discovery.nostr.enabled = false;
config.node.discovery.nostr.advertise = false;
config.node.discovery.nostr.advert_relays.clear();
config.node.discovery.nostr.stun_servers.clear();
config.node.discovery.local.enabled = false;
config.node.discovery.lan.enabled = false;
config.peers = dialing_peers(public_peers);
config.transports.udp = TransportInstances::Single(UdpConfig {
bind_addr: Some("0.0.0.0:0".into()),
advertise_on_nostr: Some(false),
public: Some(false),
accept_connections: Some(false),
..Default::default()
});
let seed_urls = source
.transports
.websocket
.iter()
.flat_map(|(_, config)| config.seed_urls.clone())
.collect::<Vec<_>>();
if !seed_urls.is_empty() {
config.transports.websocket = TransportInstances::Single(WebSocketConfig {
seed_urls,
..Default::default()
});
}
Self { config }
}
async fn bind(self) -> anyhow::Result<Arc<FipsEndpoint>> {
let builder = FipsEndpoint::builder()
.config(self.config)
.identity_nsec(Keys::generate().secret_key().to_secret_hex())
.without_system_tun();
Ok(Arc::new(builder.bind().await?))
}
}
fn dialing_peers(peers: Vec<PeerConfig>) -> Vec<PeerConfig> {
peers
.into_iter()
.filter_map(|mut peer| {
peer.npub = nostr::PublicKey::parse(&peer.npub).ok()?.to_bech32().ok()?;
peer.alias = None;
Some(peer)
})
.collect()
}
fn admission_policy(local_npub: &str, siblings: &[fips_core::PeerIdentity]) -> PeerAdmissionConfig {
let mut denied_npubs = vec![local_npub.to_owned()];
denied_npubs.extend(siblings.iter().map(|peer| peer.npub()));
denied_npubs.sort();
denied_npubs.dedup();
PeerAdmissionConfig {
configured_only: false,
denied_npubs,
}
}
struct ReaderState {
endpoint: Arc<FipsEndpoint>,
transport: Option<Arc<TcpBlobTransport<AttachmentChunkStore>>>,
}
impl ReaderState {
async fn bind(
options: ProviderReadOptions,
primary: Arc<AttachmentChunkStore>,
) -> anyhow::Result<Self> {
let endpoint = options.bind().await?;
match TcpBlobTransport::bind_client_with_config(
endpoint.clone(),
primary,
TcpBlobTransportConfig::default(),
)
.await
{
Ok(transport) => Ok(Self {
endpoint,
transport: Some(Arc::new(transport)),
}),
Err(error) => {
let _ = endpoint.shutdown().await;
Err(error.into())
}
}
}
}
pub(super) struct ProviderReads {
state: RwLock<ReaderState>,
options: ProviderReadOptions,
primary: Arc<AttachmentChunkStore>,
shutdown: std::sync::atomic::AtomicBool,
public_peers: RwLock<Vec<PeerConfig>>,
applied_peers: tokio::sync::Mutex<Option<Vec<u8>>>,
local_npub: String,
siblings: Arc<RwLock<Vec<fips_core::PeerIdentity>>>,
}
impl ProviderReads {
pub(super) async fn bind(
mut options: ProviderReadOptions,
primary: Arc<AttachmentChunkStore>,
local_npub: String,
siblings: Arc<RwLock<Vec<fips_core::PeerIdentity>>>,
) -> anyhow::Result<Self> {
{
let excluded = siblings.read().unwrap_or_else(|error| error.into_inner());
options.config.node.peer_admission = admission_policy(&local_npub, &excluded);
options.config.peers.retain(|peer| {
peer.npub != local_npub
&& !excluded.iter().any(|sibling| sibling.npub() == peer.npub)
});
}
let public_peers = options.config.peers.clone();
let state = ReaderState::bind(options.clone(), primary.clone()).await?;
Ok(Self {
state: RwLock::new(state),
options,
primary,
shutdown: std::sync::atomic::AtomicBool::new(false),
public_peers: RwLock::new(public_peers),
applied_peers: tokio::sync::Mutex::new(None),
local_npub,
siblings,
})
}
pub(super) fn endpoint(&self) -> Arc<FipsEndpoint> {
self.state
.read()
.unwrap_or_else(|error| error.into_inner())
.endpoint
.clone()
}
pub(super) fn transport(&self) -> anyhow::Result<Arc<TcpBlobTransport<AttachmentChunkStore>>> {
self.state
.read()
.unwrap_or_else(|error| error.into_inner())
.transport
.clone()
.ok_or_else(|| anyhow::anyhow!("private attachment reads are closed"))
}
async fn ready_reader(
&self,
applied: &mut Option<Vec<u8>>,
) -> anyhow::Result<Arc<FipsEndpoint>> {
anyhow::ensure!(
!self.shutdown.load(Ordering::Acquire),
"private attachment reads are closed"
);
let endpoint = self.endpoint();
if self.transport().is_ok() && !endpoint.is_stopped().await {
return Ok(endpoint);
}
self.close_reader().await;
*applied = None;
let mut options = self.options.clone();
options.config.peers = self
.public_peers
.read()
.unwrap_or_else(|error| error.into_inner())
.iter()
.filter(|peer| self.permits(&peer.npub))
.cloned()
.collect();
options.config.node.peer_admission = admission_policy(
&self.local_npub,
&self
.siblings
.read()
.unwrap_or_else(|error| error.into_inner()),
);
let replacement = ReaderState::bind(options, self.primary.clone()).await?;
if self.shutdown.load(Ordering::Acquire) {
let _ = replacement.endpoint.shutdown().await;
if let Some(transport) = replacement.transport {
if let Ok(transport) = Arc::try_unwrap(transport) {
let _ = transport.shutdown().await;
}
}
anyhow::bail!("private attachment reads are closed");
}
let endpoint = replacement.endpoint.clone();
*self
.state
.write()
.unwrap_or_else(|error| error.into_inner()) = replacement;
Ok(endpoint)
}
pub(super) fn permits(&self, npub: &str) -> bool {
npub != self.local_npub
&& !self
.siblings
.read()
.unwrap_or_else(|error| error.into_inner())
.iter()
.any(|sibling| sibling.npub() == npub)
}
pub(super) fn update_peers(&self, peers: Vec<PeerConfig>) {
*self
.public_peers
.write()
.unwrap_or_else(|error| error.into_inner()) = dialing_peers(peers);
}
pub(super) async fn set_siblings(
&self,
siblings: Vec<fips_core::PeerIdentity>,
) -> anyhow::Result<()> {
let mut applied = self.applied_peers.lock().await;
if *self
.siblings
.read()
.unwrap_or_else(|error| error.into_inner())
== siblings
{
self.ready_reader(&mut applied).await?;
return Ok(());
}
let result = self
.endpoint()
.update_peer_admission(admission_policy(&self.local_npub, &siblings))
.await;
if result.is_err() {
self.close_reader().await;
}
*self
.siblings
.write()
.unwrap_or_else(|error| error.into_inner()) = siblings;
*applied = None;
if let Err(error) = result {
crate::perflog!("attachment.provider.admission.rebind={error}");
}
self.ready_reader(&mut applied).await?;
Ok(())
}
pub(super) async fn prepare_paths(
&self,
discovery: &FipsEndpoint,
local_providers: &[String],
providers: &[String],
) -> Result<Arc<TcpBlobTransport<AttachmentChunkStore>>, StoreError> {
if self.shutdown.load(Ordering::Acquire) {
return Err(StoreError::Other(
"private attachment reads are closed".into(),
));
}
let local_hints = futures_util::future::try_join_all(
local_providers
.iter()
.filter(|npub| self.permits(npub))
.map(|npub| async move {
let identity = fips_core::PeerIdentity::from_npub(npub)
.map_err(|error| StoreError::Other(error.to_string()))?;
let addresses = discovery
.authenticated_local_udp_listen_addrs(identity)
.await
.map_err(|error| StoreError::Other(error.to_string()))?;
Ok::<_, StoreError>((npub, addresses))
}),
)
.await?;
let mut applied = self.applied_peers.lock().await;
let endpoint = self
.ready_reader(&mut applied)
.await
.map_err(|error| StoreError::Other(error.to_string()))?;
let mut peers = self
.public_peers
.read()
.unwrap_or_else(|error| error.into_inner())
.clone();
peers.retain(|peer| self.permits(&peer.npub));
for (npub, addresses) in local_hints {
if !self.permits(npub) {
continue;
}
for address in addresses {
let hint =
fips_core::config::PeerAddress::new("udp", address.to_string()).authenticated();
if let Some(existing) = peers.iter_mut().find(|candidate| &candidate.npub == npub) {
if !existing.addresses.contains(&hint) {
existing.addresses.push(hint);
}
} else {
peers.push(PeerConfig {
npub: npub.clone(),
addresses: vec![hint],
..Default::default()
});
}
}
}
for npub in providers {
if self.permits(npub) && !peers.iter().any(|peer| &peer.npub == npub) {
peers.push(PeerConfig {
npub: npub.clone(),
..Default::default()
});
}
}
for peer in &mut peers {
peer.alias = None;
}
peers.sort_by(|left, right| left.npub.cmp(&right.npub));
let fingerprint =
serde_json::to_vec(&peers).map_err(|error| StoreError::Other(error.to_string()))?;
if applied.as_ref() != Some(&fingerprint) {
endpoint
.update_peers(peers)
.await
.map_err(|error| StoreError::Other(error.to_string()))?;
*applied = Some(fingerprint);
}
self.transport()
.map_err(|error| StoreError::Other(error.to_string()))
}
pub(super) async fn shutdown(&self) {
self.shutdown.store(true, Ordering::Release);
let _applied = self.applied_peers.lock().await;
self.close_reader().await;
}
async fn close_reader(&self) {
let (endpoint, transport) = {
let mut state = self
.state
.write()
.unwrap_or_else(|error| error.into_inner());
(state.endpoint.clone(), state.transport.take())
};
let _ = endpoint.shutdown().await;
if let Some(transport) = transport {
if let Ok(transport) = Arc::try_unwrap(transport) {
let _ = transport.shutdown().await;
}
}
}
}
#[cfg(test)]
#[path = "provider_reads_tests.rs"]
mod tests;