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,