Skip to content

Bridge K-lines into Matrix policy lists #1832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ ircService:
# to identify banned users, rooms and servers.
rooms:
- "#matrix-org-coc-bl:matrix.org"
# Optional: Rooms to sync K-lined users to
serverBanLists:
# Keys are domains matching entries in ircService.servers
irc.example.com: "#exampleircd:matrix.org"

# Options here are generally only applicable to large-scale bridges and may have
# consequences greater than other options in this configuration file.
Expand Down
66 changes: 33 additions & 33 deletions spec/unit/MatrixBanSync.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,39 @@ const BANNED_SERVER_ENTITY = {

describe("MatrixBanSync", () => {
let banSync;
const intent = {
join: async (roomOrAlias) => {
if (roomOrAlias.includes('valid:room')) {
return roomOrAlias.replace('#', '!');
}
throw Error('Room not known');
},
roomState: async (roomId) => {
if (roomId === '!valid:room') {
return [
{
type: 'm.policy.rule.server',
state_key: 'should-not-be-here',
content: {
recommendation: 'still-not-interested',
entity: 'foo.com',
reason: 'foo',
}
},
BANNED_SERVER_STATE_EVENT,
];
}
else if (roomId === '!anothervalid:room') {
return [
{type: 'not-interested'},
BANNED_USER_STATE_EVENT,
];
}
throw Error('Unknown room');
},
};
beforeEach(() => {
banSync = new MatrixBanSync({ rooms: [] });
banSync = new MatrixBanSync(intent, { rooms: [] });
banSync.interestingRooms = new Set(["!valid:room"]);
})
describe("isUserBanned", () => {
Expand Down Expand Up @@ -106,38 +137,7 @@ describe("MatrixBanSync", () => {
describe("syncRules", () => {
it("should sync state from a set of rooms", async () => {
banSync.config.rooms = ["!valid:room", "#anothervalid:room", "!notvalid:room"];
const intent = {
join: async (roomOrAlias) => {
if (roomOrAlias.includes('valid:room')) {
return roomOrAlias.replace('#', '!');
}
throw Error('Room not known');
},
roomState: async (roomId) => {
if (roomId === '!valid:room') {
return [
{
type: 'm.policy.rule.server',
state_key: 'should-not-be-here',
content: {
recommendation: 'still-not-interested',
entity: 'foo.com',
reason: 'foo',
}
},
BANNED_SERVER_STATE_EVENT,
];
}
else if (roomId === '!anothervalid:room') {
return [
{type: 'not-interested'},
BANNED_USER_STATE_EVENT,
];
}
throw Error('Unknown room');
},
}
await banSync.syncRules(intent);
await banSync.syncRules();
expect(banSync.bannedEntites.size).toEqual(2);
expect(
banSync.bannedEntites.get(`!valid:room:banned-server`)
Expand Down
11 changes: 7 additions & 4 deletions src/bridge/IrcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class IrcBridge {
public readonly publicitySyncer: PublicitySyncer;
public activityTracker: ActivityTracker|null = null;
public readonly roomConfigs: RoomConfig;
public readonly matrixBanSyncer?: MatrixBanSync;
public matrixBanSyncer?: MatrixBanSync;
private _mediaProxy?: MediaProxy;
private clientPool!: ClientPool; // This gets defined in the `run` function
private ircServers: IrcServer[] = [];
Expand Down Expand Up @@ -212,7 +212,6 @@ export class IrcBridge {
maxActionDelayMs: 5 * 60 * 1000, // 5 mins,
defaultTtlMs: 10 * 60 * 1000, // 10 mins
});
this.matrixBanSyncer = this.config.ircService.banLists && new MatrixBanSync(this.config.ircService.banLists);
this.matrixHandler = new MatrixHandler(this, this.config.ircService.matrixHandler, this.membershipQueue);
this.privacyProtection = new PrivacyProtection(this);
this.ircHandler = new IrcHandler(
Expand Down Expand Up @@ -289,7 +288,7 @@ export class IrcBridge {
this.config.ircService.logging = newConfig.ircService.logging;
}

const banSyncPromise = this.matrixBanSyncer?.syncRules(this.bridge.getIntent());
const banSyncPromise = this.matrixBanSyncer?.syncRules();

await this.dataStore.removeConfigMappings();

Expand Down Expand Up @@ -635,7 +634,11 @@ export class IrcBridge {
}

await this.bridge.initialise();
await this.matrixBanSyncer?.syncRules(this.bridge.getIntent());
this.matrixBanSyncer = this.config.ircService.banLists && new MatrixBanSync(
this.bridge.getIntent(),
this.config.ircService.banLists,
);
await this.matrixBanSyncer?.syncRules();
this.matrixHandler.initialise();

this.activityTracker = new ActivityTracker(this.bridge.getIntent().matrixClient, {
Expand Down
49 changes: 43 additions & 6 deletions src/bridge/MatrixBanSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getLogger } from "../logging";
const log = getLogger("MatrixBanSync");
export interface MatrixBanSyncConfig {
rooms: string[];
serverBanLists?: { [ircServer: string]: string };
}

enum BanEntityType {
Expand Down Expand Up @@ -53,16 +54,37 @@ const supportedRecommendations = [
export class MatrixBanSync {
private bannedEntites = new Map<string, BanEntity>();
private subscribedRooms = new Set<string>();
constructor(private config: MatrixBanSyncConfig) { }
private ircServerBanRooms = new Map<string, string>();
constructor(private intent: Intent, private config: MatrixBanSyncConfig) { }

public async syncRules(intent: Intent) {
public async markUserAsBanned(ircServer: string, mxid: string, reason = "k-lined"): Promise<void> {
this.bannedEntites.set(mxid, {
matcher: new MatrixGlob(mxid),
entityType: BanEntityType.User,
reason: reason,
});

const roomId = this.ircServerBanRooms.get(ircServer);
if (!roomId) {
log.warn(`No serverBanList configured for ${ircServer}; Matrix policy will not be created for ${mxid}`);
return;
}

await this.intent.sendStateEvent(roomId, 'm.policy.rule.user', `rule:${mxid}`, {
"entity": mxid,
"reason": reason,
"recommendation": "m.ban"
});
}

public async syncRules() {
this.bannedEntites.clear();
this.subscribedRooms.clear();
for (const roomIdOrAlias of this.config.rooms) {
try {
const roomId = await intent.join(roomIdOrAlias);
const roomId = await this.intent.join(roomIdOrAlias);
this.subscribedRooms.add(roomId);
const roomState = await intent.roomState(roomId, false) as WeakStateEvent[];
const roomState = await this.intent.roomState(roomId, false) as WeakStateEvent[];
for (const evt of roomState) {
this.handleIncomingState(evt, roomId);
}
Expand All @@ -71,6 +93,20 @@ export class MatrixBanSync {
log.error(`Failed to read ban list from ${roomIdOrAlias}`, ex);
}
}
for (const [ircServer, roomIdOrAlias] of Object.entries(this.config.serverBanLists ?? {})) {
try {
const roomId = await this.intent.join(roomIdOrAlias);
this.subscribedRooms.add(roomId);
this.ircServerBanRooms.set(ircServer, roomId);
const roomState = await this.intent.roomState(roomId, false) as WeakStateEvent[];
for (const evt of roomState) {
this.handleIncomingState(evt, roomId);
}
}
catch (ex) {
log.error(`Failed to sync rules for ${ircServer} in ${roomIdOrAlias}:`, ex);
}
}
}

/**
Expand Down Expand Up @@ -139,8 +175,9 @@ export class MatrixBanSync {
* @param config The new config.
* @param intent The bot user intent.
*/
public async updateConfig(config: MatrixBanSyncConfig, intent: Intent) {
// XXX not actually called, ever: hot reloading likely broken
public async updateConfig(config: MatrixBanSyncConfig) {
this.config = config;
await this.syncRules(intent);
await this.syncRules();
}
}
11 changes: 11 additions & 0 deletions src/irc/ClientPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ export class ClientPool {
// Remove client if we failed to connect!
this.removeBridgedClient(bridgedClient);
}
if (err instanceof IRCConnectionError && err.code === IRCConnectionErrorCode.Banned) {
void this.ircBridge.matrixBanSyncer?.markUserAsBanned(server.domain, userId).catch((mbsErr: Error) => {
log.error(`Failed to mark ${userId} as banned: ${mbsErr.toString}`);
});
}
// If we failed to connect
log.error("Couldn't connect virtual user %s (%s) to %s : %s",
ircClientConfig.getDesiredNick(), userId, server.domain, JSON.stringify(err));
Expand Down Expand Up @@ -676,6 +681,12 @@ export class ClientPool {
}

if (disconnectReason === "banned" && userId) {
void this.ircBridge.matrixBanSyncer?.markUserAsBanned(
bridgedClient.server.domain,
userId,
).catch((err: Error) => {
log.error(`Failed to mark ${userId} as banned: ${err.toString}`);
});
const req = new BridgeRequest(this.ircBridge.getAppServiceBridge().getRequestFactory().newRequest());
this.ircBridge.matrixHandler.quitUser(
req, userId, [bridgedClient],
Expand Down
Loading