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?" clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
blockedInstances: "Blocked Instances" blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\ 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" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"

View file

@ -1,7 +1,5 @@
import { Brackets } from 'typeorm';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { Instances } from '@/models/index.js';
import { Instance } from '@/models/entities/instance.js'; import { Instance } from '@/models/entities/instance.js';
import { DAY } from '@/const.js'; import { DAY } from '@/const.js';
@ -9,17 +7,35 @@ import { DAY } from '@/const.js';
// "dead" and should no longer get activities delivered to it. // "dead" and should no longer get activities delivered to it.
const deadThreshold = 7 * DAY; 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. * 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 * @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']> { export async function skippedInstances(hosts: Array<Instance['host']>): Promise<Array<Instance['host']>> {
// first check for blocked instances since that info may already be in memory const skipped = hosts.filter(host => shouldBlockInstance(host));
const { blockedHosts } = await fetchMeta();
const skipped = hosts.filter(host => blockedHosts.includes(host));
// if possible return early and skip accessing the database // 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)); ```
@ -32,10 +48,10 @@ export async function skippedInstances(hosts: Array<Instace['host']>): Array<Ins
deadTime.toISOString(), deadTime.toISOString(),
// don't check hosts again that we already know are suspended // don't check hosts again that we already know are suspended
// also avoids adding duplicates to the list // 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 * @param host punycoded instance host
* @returns whether the given host should be skipped * @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]); const skipped = await skippedInstances([host]);
return skipped.length > 0; 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 { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
import { Instances } from '@/models/index.js'; import { Instances } from '@/models/index.js';
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/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 { toPuny, extractDbHost } from '@/misc/convert-host.js';
import { getApId } from '@/remote/activitypub/type.js'; import { getApId } from '@/remote/activitypub/type.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.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 { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js'; import { UserPublickey } from '@/models/entities/user-publickey.js';
import { InboxJobData } from '@/queue/types.js'; import { InboxJobData } from '@/queue/types.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
const logger = new Logger('inbox'); 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 host = toPuny(new URL(signature.keyId).hostname);
// ブロックしてたら中断 // Stop if the host is blocked.
const meta = await fetchMeta(); if (await shouldBlockInstance(host)) {
if (meta.blockedHosts.includes(host)) {
return `Blocked request: ${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})`; return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
} }
// ブロックしてたら中断 // Stop if the host is blocked.
const ldHost = extractDbHost(authUser.user.uri); const ldHost = extractDbHost(authUser.user.uri);
if (meta.blockedHosts.includes(ldHost)) { if (await shouldBlockInstance(ldHost)) {
return `Blocked request: ${ldHost}`; return `Blocked request: ${ldHost}`;
} }
} else { } else {

View file

@ -12,6 +12,7 @@ import renderQuestion from '@/remote/activitypub/renderer/question.js';
import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
import { signedGet } from './request.js'; import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { parseUri } from './db-resolver.js'; import { parseUri } from './db-resolver.js';
@ -67,8 +68,7 @@ export default class Resolver {
return await this.resolveLocal(value); return await this.resolveLocal(value);
} }
const meta = await fetchMeta(); if (await shouldBlockInstance(host)) {
if (meta.blockedHosts.includes(host)) {
throw new Error('Instance is blocked'); 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 { createPerson } from '@/remote/activitypub/models/person.js';
import { createNote } from '@/remote/activitypub/models/note.js'; import { createNote } from '@/remote/activitypub/models/note.js';
import DbResolver from '@/remote/activitypub/db-resolver.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 { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { CacheableLocalUser, User } from '@/models/entities/user.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 { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
import { SchemaType } from '@/misc/schema.js'; import { SchemaType } from '@/misc/schema.js';
import { HOUR } from '@/const.js'; import { HOUR } from '@/const.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
import define from '../../define.js'; import define from '../../define.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -85,9 +84,11 @@ export default define(meta, paramDef, async (ps, me) => {
* URIからUserかNoteを解決する * URIからUserかNoteを解決する
*/ */
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断 // Stop if the host is blocked.
const fetchedMeta = await fetchMeta(); const host = extractDbHost(uri);
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null; if (await shouldBlockInstance(host)) {
return null;
}
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();