server: block subdomains of a blocked host #259

Closed
norm wants to merge 7 commits from wildcard-block into main
5 changed files with 42 additions and 26 deletions

View file

@ -187,7 +187,7 @@ clearCachedFiles: "Clear cache"
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ block. Listed instances will no longer be able to communicate with this instance."
\ block. Listed instances will no longer be able to communicate with this instance. This also blocks subdomains as well."
muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users"
blockedUsers: "Blocked users"

View file

@ -1,7 +1,5 @@
import { Brackets } from 'typeorm';
import { db } from '@/db/postgre.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Instances } from '@/models/index.js';
import { Instance } from '@/models/entities/instance.js';
import { DAY } from '@/const.js';
@ -9,19 +7,37 @@ import { DAY } from '@/const.js';
// "dead" and should no longer get activities delivered to it.
const deadThreshold = 7 * DAY;
/**
* Returns whether a host is a subdomain of another or is the same.
* @param host punycoded instance host
* @param blockedHost punycoded instance host that is blocked
* @returns whether host and blockedHost is the same or if host is a subdomain of blockedHost
*/
function sameOrSubdomainOf(host: Instance['host'], blockedHost: Instance['host']): boolean {
return host === blockedHost || host.endsWith('.' + blockedHost);
}
/**
* Returns whether a specific host (punycoded) should be blocked.
*
* @param host punycoded instance host
* @returns whether the given host should be blocked
*/
export async function shouldBlockInstance(host: Instance['host']): Promise<boolean> {
const { blockedHosts } = await fetchMeta();
return blockedHosts.some(blockedHost => sameOrSubdomainOf(host, blockedHost));
}
/**
* Returns the subset of hosts which should be skipped.
norm marked this conversation as resolved Outdated

Being suspended does not mean incoming activities will be dropped.

Being suspended does not mean incoming activities will be dropped.
*
* @param hosts array of punycoded instance hosts
* @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter)
* @returns array of punycoded instance hosts that should be skipped (subset of hosts parameter)
*/
export async function skippedInstances(hosts: Array<Instace['host']>): Array<Instance['host']> {
// first check for blocked instances since that info may already be in memory
const { blockedHosts } = await fetchMeta();
const skipped = hosts.filter(host => blockedHosts.includes(host));
export async function skippedInstances(hosts: Array<Instance['host']>): Promise<Array<Instance['host']>> {
const skipped = hosts.filter(host => shouldBlockInstance(host));
// if possible return early and skip accessing the database
if (skipped.length === hosts.length) return hosts;
if (skipped.length === hosts.length) return hosts;
norm marked this conversation as resolved Outdated
-	const skipped = hosts.filter(host => blockedHosts.some(blockedHost => sameOrSubdomainOf(host, blockedHost)));
+	const skipped = hosts.filter(host => shouldBlockInstance(host));
```diff - const skipped = hosts.filter(host => blockedHosts.some(blockedHost => sameOrSubdomainOf(host, blockedHost))); + const skipped = hosts.filter(host => shouldBlockInstance(host)); ```
const deadTime = new Date(Date.now() - deadThreshold);
@ -32,10 +48,10 @@ export async function skippedInstances(hosts: Array<Instace['host']>): Array<Ins
deadTime.toISOString(),
// don't check hosts again that we already know are suspended
// also avoids adding duplicates to the list
hosts.filter(host => !skipped.includes(host) && !host.includes(',')).join(','),
hosts.filter(host => !skipped.some(blockedHost => sameOrSubdomainOf(host, blockedHost)) && !host.includes(',')).join(','),
],
)
.then(res => res.map(row => row.host))
.then(res => res.map(row => row.host)),
);
}
@ -47,7 +63,7 @@ export async function skippedInstances(hosts: Array<Instace['host']>): Array<Ins
* @param host punycoded instance host
* @returns whether the given host should be skipped
*/
export async function shouldSkipInstance(host: Instance['host']): boolean {
export async function shouldSkipInstance(host: Instance['host']): Promise<boolean> {
const skipped = await skippedInstances([host]);
return skipped.length > 0;
}

View file

@ -6,7 +6,6 @@ import Logger from '@/services/logger.js';
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
import { Instances } from '@/models/index.js';
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { toPuny, extractDbHost } from '@/misc/convert-host.js';
import { getApId } from '@/remote/activitypub/type.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
@ -17,6 +16,7 @@ import { StatusError } from '@/misc/fetch.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { InboxJobData } from '@/queue/types.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
const logger = new Logger('inbox');
@ -33,9 +33,8 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
const host = toPuny(new URL(signature.keyId).hostname);
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
// Stop if the host is blocked.
if (await shouldBlockInstance(host)) {
return `Blocked request: ${host}`;
}
@ -117,9 +116,9 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
}
// ブロックしてたら中断
// Stop if the host is blocked.
const ldHost = extractDbHost(authUser.user.uri);
if (meta.blockedHosts.includes(ldHost)) {
if (await shouldBlockInstance(ldHost)) {
return `Blocked request: ${ldHost}`;
}
} else {

View file

@ -12,6 +12,7 @@ import renderQuestion from '@/remote/activitypub/renderer/question.js';
import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { parseUri } from './db-resolver.js';
@ -67,8 +68,7 @@ export default class Resolver {
return await this.resolveLocal(value);
}
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
if (await shouldBlockInstance(host)) {
throw new Error('Instance is blocked');
}

View file

@ -1,4 +1,3 @@
import config from '@/config/index.js';
import { createPerson } from '@/remote/activitypub/models/person.js';
import { createNote } from '@/remote/activitypub/models/note.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
@ -7,10 +6,10 @@ import { extractDbHost } from '@/misc/convert-host.js';
import { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
import { SchemaType } from '@/misc/schema.js';
import { HOUR } from '@/const.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
@ -85,9 +84,11 @@ export default define(meta, paramDef, async (ps, me) => {
* URIからUserかNoteを解決する
*/
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const fetchedMeta = await fetchMeta();
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
// Stop if the host is blocked.
const host = extractDbHost(uri);
if (await shouldBlockInstance(host)) {
return null;
}
const dbResolver = new DbResolver();