From 5880a52a47db18496faa84558460a341119d3771 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 20 Jan 2026 10:43:47 +0100 Subject: [PATCH] Improve error message, allow local IP federation via env var (fixes #152) --- src/activity_queue.rs | 6 +++--- src/activity_sending.rs | 4 ++-- src/config.rs | 34 +++++++++++++++++----------------- src/error.rs | 3 +++ 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/activity_queue.rs b/src/activity_queue.rs index 8f17d4f..792151f 100644 --- a/src/activity_queue.rs +++ b/src/activity_queue.rs @@ -33,10 +33,10 @@ use url::Url; /// /// - `activity`: The activity to be sent, gets converted to json /// - `private_key`: Private key belonging to the actor who sends the activity, for signing HTTP -/// signature. Generated with [crate::http_signatures::generate_actor_keypair]. +/// signature. Generated with [crate::http_signatures::generate_actor_keypair]. /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor -/// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] -/// for each target actor. +/// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] +/// for each target actor. pub async fn queue_activity( activity: &A, actor: &ActorType, diff --git a/src/activity_sending.rs b/src/activity_sending.rs index b734088..14466e7 100644 --- a/src/activity_sending.rs +++ b/src/activity_sending.rs @@ -52,8 +52,8 @@ impl SendActivityTask { /// /// - `activity`: The activity to be sent, gets converted to json /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor - /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] - /// for each target actor. + /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] + /// for each target actor. pub async fn prepare( activity: &A, actor: &ActorType, diff --git a/src/config.rs b/src/config.rs index 9eb0b97..bd3bc23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,7 @@ use async_trait::async_trait; use bytes::Bytes; use derive_builder::Builder; use dyn_clone::{clone_trait_object, DynClone}; +use itertools::Itertools; use moka::future::Cache; use regex::Regex; use reqwest::{redirect::Policy, Client, Request}; @@ -186,27 +187,26 @@ impl FederationConfig { // Resolve domain and see if it points to private IP // TODO: Use is_global() once stabilized // https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global - let invalid_ip = - lookup_host((domain.to_owned(), 80)) - .await? - .any(|addr| match addr.ip() { - IpAddr::V4(addr) => { - addr.is_private() - || addr.is_link_local() - || addr.is_loopback() - || addr.is_multicast() - } - IpAddr::V6(addr) => { - addr.is_loopback() + let mut ips = lookup_host((domain.to_owned(), 80)).await?; + let allow_local = std::env::var("APUB_DANGER_ALLOW_LOCAL_IP").is_ok(); + let invalid_ip = !allow_local + && ips.any(|addr| match addr.ip() { + IpAddr::V4(addr) => { + addr.is_private() + || addr.is_link_local() + || addr.is_loopback() + || addr.is_multicast() + } + IpAddr::V6(addr) => { + addr.is_loopback() || addr.is_multicast() || ((addr.segments()[0] & 0xfe00) == 0xfc00) // is_unique_local || ((addr.segments()[0] & 0xffc0) == 0xfe80) // is_unicast_link_local - } - }); + } + }); if invalid_ip { - return Err(Error::UrlVerificationError( - "Localhost is only allowed in debug mode", - )); + let ip_addrs = ips.join(", "); + return Err(Error::DomainResolveError(domain.to_string(), ip_addrs)); } } diff --git a/src/error.rs b/src/error.rs index 0661071..1490c8c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,9 @@ pub enum Error { /// url verification error #[error("URL failed verification: {0}")] UrlVerificationError(&'static str), + /// Resolving domain points to local IP. + #[error("Resolving domain {0} points to local IP {1}. This may indicate an attacker attempting to access internal services. If intentional, you can ignore this error by setting DANGER_APUB_ALLOW_LOCAL_IP=1")] + DomainResolveError(String, String), /// Incoming activity has invalid digest for body #[error("Incoming activity has invalid digest for body")] ActivityBodyDigestInvalid,