Domain blocks: suspend, silence, reject media

DomainBlock (domain, severity, reject-media, public and private comment)
covers the domain and its subdomains. Admins manage them under
/clientapi/admin/domainblocks/{list,insert,delete}; the set is kept in
memory, reloaded on every change and at most five minutes stale.

A suspended domain is refused by FederationHttp.IsAllowed, so nothing is
fetched from it and no job delivers to it, and the inbox drops its
activities with a 202 before fetching any key. Reject-media strips the
attachments of posts from that domain. Silence is recorded for the
timelines and notifications that arrive in P1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 11:25:05 +02:00
1 parent 9ec1f9930b
commit e6a362c0b8
13 files changed
+318 -13

No files matched your search

@@ -4,16 +4,18 @@ using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Entities;
using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Inbox;
using PrivaPub.Federation.Objects;
using PrivaPub.Federation.Inbox.Handlers;
using PrivaPub.Models.Jobs;
using PrivaPub.Federation.Inbox;
using PrivaPub.Federation.Moderation;
using PrivaPub.Federation.Objects;
using PrivaPub.Federation.Outbox;
using PrivaPub.Infrastructure.Jobs;
using PrivaPub.Models;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Group;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.Post;
using PrivaPub.Models.User;
using PrivaPub.Models;
using PrivaPub.StaticServices;
using PrivaPub.Tests.Support;
@@ -33,6 +35,7 @@ namespace PrivaPub.Tests.Federation
LocalActorService _local;
InboxReceiver _receiver;
InboxProcessor _processor;
DomainBlocks _blocks;
public async ValueTask InitializeAsync()
{
@@ -44,12 +47,13 @@ namespace PrivaPub.Tests.Federation
var queue = new JobQueue();
var delivery = new DeliveryService(new DbEntities(), queue);
var db = new DbEntities();
_receiver = new InboxReceiver(_local, remote, queue, NullLogger<InboxReceiver>.Instance);
_blocks = new DomainBlocks(NullLogger<DomainBlocks>.Instance);
_receiver = new InboxReceiver(_local, remote, queue, _blocks, NullLogger<InboxReceiver>.Instance);
_processor = new InboxProcessor(remote, new IActivityHandler[]
{
new FollowHandler(db, _local, remote, delivery),
new UndoHandler(db, _local, remote, delivery),
new CreateHandler(db, _local, remote, delivery),
new CreateHandler(db, _local, remote, delivery, _blocks),
new DeleteHandler(db, _local, remote, delivery),
new UpdateHandler(db, _local, remote)
}, NullLogger<InboxProcessor>.Instance);
@@ -254,6 +258,46 @@ namespace PrivaPub.Tests.Federation
Assert.Equal(alice.Id, mention.AccountId);
}
[Fact]
public async Task A_suspended_domain_is_dropped_before_its_key_is_fetched()
{
var token = TestContext.Current.CancellationToken;
var alice = await LocalAvatar("alice");
var bob = new RemoteActor(_peer, "bob", _peer.B);
await DB.Default.SaveAsync(new DomainBlock { Domain = "localhost", Severity = DomainBlockSeverity.Suspend }, token);
try
{
await _blocks.Reload(token);
var before = _peer.Requests.Count;
var result = await Deliver(bob, $"/peasants/{alice.UserName}/mouth", DirectCreate(bob, alice.Uri));
Assert.Equal(202, result.StatusCode);
Assert.Equal(before, _peer.Requests.Count);
Assert.False(await DB.Default.Find<Post>().Match(p => p.ActorURI == bob.Id).ExecuteAnyAsync(token));
}
finally
{
await DB.Default.DeleteAsync<DomainBlock>(b => b.Domain == "localhost");
await _blocks.Reload(token);
}
}
[Fact]
public void A_block_covers_subdomains_but_not_lookalikes()
{
var blocks = new DomainBlocks(NullLogger<DomainBlocks>.Instance);
typeof(DomainBlocks).GetField("_blocks", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
.SetValue(blocks, new Dictionary<string, DomainBlock> { ["evil.example"] = new() { Domain = "evil.example", Severity = DomainBlockSeverity.Silence } });
typeof(DomainBlocks).GetField("_loadedAt", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
.SetValue(blocks, DateTime.UtcNow);
Assert.NotNull(blocks.Find("evil.example"));
Assert.NotNull(blocks.Find("A.Evil.Example."));
Assert.Null(blocks.Find("notevil.example"));
Assert.False(blocks.IsSuspended("evil.example"));
}
[Fact]
public async Task A_bad_signature_is_a_401()
{