Skip to content
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

Replace Synapse antispam module with a "rule server" counterpart #78

Closed
wants to merge 2 commits into from
Closed
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
53 changes: 28 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,27 @@ Using the bot to manage your rooms is great, however if you want to use your ban
is needed. Primarily meant to block invites from undesired homeservers/users, Mjolnir's
Synapse module is a way to interpret ban lists and apply them to your entire homeserver.

First, install the module to your Synapse python environment:
**Warning**: This module works by running generated Python code in your homeserver. The code
is generated based off the rules provided in the ban lists, which may include crafted rules
which may allow unrestricted access to your server. Only use lists you trust and do not
use anyone else's infrastructure to get the ruleset from - only use infrastructure that
you control.

If this is acceptable, the following steps may be performed.

First, run the Docker image for the rule server. This is what will be serving the generated
Python for the Synapse antispam module to read from. This rule server will serve the Python
off a webserver at `/api/v1/py_rules` which must be accessible by wherever Synapse is installed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, the rules server appears to serve the rules from any endpoint, not just /api/v1/py_rules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, that's technically a bug but the documentation should be what people expect to work.

It is not recommended to expose this webserver to the outside world.

```
docker run --rm -d -v /etc/mjolnir/ruleserver.yaml:/data/config/production.yaml -p 127.0.0.0:8080:8080 matrixdotorg/mjolnir
```

**Note**: the exact same Mjolnir image is used to run the rule server. To configure using the rule
server instead of the bot function, see the `ruleServer` options in the config.

After that is running, install the module to your Synapse python environment:
```
pip install -e "git+https://github.com/matrix-org/mjolnir.git#egg=mjolnir&subdirectory=synapse_antispam"
```
Expand All @@ -100,35 +120,18 @@ pip install -e "git+https://github.com/matrix-org/mjolnir.git#egg=mjolnir&subdir
Then add the following to your `homeserver.yaml`:
```yaml
spam_checker:
module: mjolnir.AntiSpam
config:
# Prevent servers/users in the ban lists from inviting users on this
# server to rooms. Default true.
block_invites: true
# Flag messages sent by servers/users in the ban lists as spam. Currently
# this means that spammy messages will appear as empty to users. Default
# false.
block_messages: false
# Remove users from the user directory search by filtering matrix IDs and
# display names by the entries in the user ban list. Default false.
block_usernames: false
# The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
# this list cannot be room aliases or permalinks. This server is expected
# to already be joined to the room - Mjolnir will not automatically join
# these rooms.
ban_lists:
- "!roomid:example.org"
- module: mjolnir.AntiSpam
config:
# Where the antispam module should periodically retrieve updated rules from. This
# should be pointed at the Mjolnir rule server.
rules_url: 'http://localhost:8080/api/v1/py_rules'
```

*Note*: Although this is described as a "spam checker", it does much more than fight
spam.

Be sure to change the configuration to match your setup. Your server is expected to
already be participating in the ban lists - if it is not, you will need to have a user
on your homeserver join. The antispam module will not join the rooms for you.

If you change the configuration, you will need to restart Synapse. You'll also need
to restart Synapse to install the plugin.
Be sure to change the configuration to match your setup. If you change the configuration,
you will need to restart Synapse. You'll also need to restart Synapse to install the plugin.

## Development

Expand Down
70 changes: 70 additions & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,73 @@ health:
# The HTTP status code which reports that the bot is not healthy/ready.
# Defaults to 418.
unhealthyStatus: 418

# Optional configuration for the built-in rule server
ruleServer:
# When enabled, all bot functionality will be disabled. This means that if you
# want to run a bot alongside a rule server then you'll need two instances of
# this config: one for the bot and one for the rule server. The rule server is
# disabled by default.
#
# Note that the config above should be as complete as possible, though the rule
# server will ignore many options not applicable to it (protections, commands, etc)
enabled: false

# The port to run the rule server on. Defaults to 8080. Note that if you're using
# the healthz options then this port must be different. For health monitoring
# without a healthz endpoint, use /api/v1/py_rules and look for a 200 OK response.
port: 8080

# The address to listen for requests on. Defaults to all addresses. Typically for
# Docker containers this will be 0.0.0.0 so the port mapping can function correctly.
address: '0.0.0.0'

# The policy rooms (matrix.to URLs) to expose on the rule server. These should be
# trusted rooms to avoid potential security risks with the Synapse module.
listRooms:
- "https://matrix.to/#/#yourroom:example.org"

# Restrictions that can be applied to users, servers, and whole rooms. These are
# split into categories (eg: 'messages') which describe the sort of action the
# entity will be taking. Under that are the kinds of entities that can be affected.
# When the flag for an entity is `true`, it means that if a rule applies to that
# kind of entity (eg: users) then it will be given to the antispam module to
# process. When false, rules which apply to that entity will not be considered for
# that action.
blocks:
# Whether or not to block messages (events) sent by an entity.
messages:
users: true
rooms: true
servers: true
# Whether or not to block invites from the given entities
invites:
users: true
rooms: true
servers: true
# Whether or not check a given user's username/displayname against the list rules.
usernames:
users: true
rooms: false # rooms don't have usernames and can't be blocked.
servers: false # the only rule which would apply is one for the local server.
Comment on lines +206 to +207
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why even have these in the default config if they don't apply?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside from the interface being easier to copy/paste and understand, mjolnir doesn't protect people from making mistakes almost by design - if someone really wanted to turn this switch on due to a massive attack of some sort, they could.

# Whether or not rooms to block rooms from being created by given entities. Rooms are
# only created on the local server - this does not trigger for rooms which have been
# 'discovered' by federation.
roomCreate:
users: true
rooms: false # rooms don't create rooms
servers: false # room creation only happens on the local server, which means banning yourself.
# Whether or not a given entity should be blocked from creating an alias pointing at a room.
# Like room creation, this only happens on the local server - remote aliases do not trigger these.
makeAlias:
users: true
rooms: true # this would block rooms from receiving aliases on the local server
servers: false # room aliases can only be created on the local server, so blocking yourself is not recommended
# Whether or not to block rooms from being published by a given entity. Like other aspects of room
# aliases and creation, this is only triggered for local attempts to publish a room to the server's
# own directory - federated directories do not trigger this.
publishRoom:
users: true
rooms: true # this would block the rooms from entering the directory
servers: false # publishing happens locally, and it's not recommended to ban yourself

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"config": "^3.3.1",
"escape-html": "^1.0.3",
"js-yaml": "^3.13.1",
"matrix-bot-sdk": "^0.5.4"
"matrix-bot-sdk": "^0.5.9"
}
}
6 changes: 6 additions & 0 deletions src/LogProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export async function logMessage(level: LogLevel, module: string, message: strin
if (!additionalRoomIds) additionalRoomIds = [];
if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds];

// Prefix logging if we're running in ruleserver mode
if (config.ruleServer?.enabled) {
module = `[RuleServer] ${module}`;
message = `[RuleServer] ${message}`;
}

if (config.RUNTIME.client && (config.verboseLogging || LogLevel.INFO.includes(level))) {
let clientMessage = message;
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
Expand Down
60 changes: 60 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ limitations under the License.
import * as config from "config";
import { MatrixClient } from "matrix-bot-sdk";

export interface IBlock {
users: boolean;
rooms: boolean;
servers: boolean;
}

export interface IRuleServerBlocks {
messages: IBlock;
invites: IBlock;
usernames: IBlock;
roomCreate: IBlock;
makeAlias: IBlock;
publishRoom: IBlock;
}

interface IConfig {
homeserverUrl: string;
accessToken: string;
Expand Down Expand Up @@ -59,6 +74,13 @@ interface IConfig {
unhealthyStatus: number;
};
};
ruleServer: {
enabled: boolean;
port: number;
address: string;
listRooms: string[];
blocks: IRuleServerBlocks;
};

/**
* Config options only set at runtime. Try to avoid using the objects
Expand Down Expand Up @@ -111,6 +133,44 @@ const defaultConfig: IConfig = {
unhealthyStatus: 418,
},
},
ruleServer: {
enabled: false,
port: 8080,
address: '0.0.0.0',
listRooms: [],
blocks: {
messages: {
users: false,
rooms: false,
servers: false,
},
invites: {
users: false,
rooms: false,
servers: false,
},
usernames: {
users: false,
rooms: false,
servers: false,
},
roomCreate: {
users: false,
rooms: false,
servers: false,
},
makeAlias: {
users: false,
rooms: false,
servers: false,
},
publishRoom: {
users: false,
rooms: false,
servers: false,
},
}
},

// Needed to make the interface happy.
RUNTIME: {
Expand Down
46 changes: 35 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { logMessage } from "./LogProxy";
import { MembershipEvent } from "matrix-bot-sdk/lib/models/events/MembershipEvent";
import * as htmlEscape from "escape-html";
import { Healthz } from "./health/healthz";
import { RuleServer } from "./modules/RuleServer";

config.RUNTIME = {client: null};

Expand All @@ -57,6 +58,40 @@ if (config.health.healthz.enabled) {

config.RUNTIME.client = client;

const joinedRooms = await client.getJoinedRooms();

// Ensure we're in the management room
LogService.info("index", "Resolving management room...");
const managementRoomId = await client.resolveRoom(config.managementRoom);
if (!joinedRooms.includes(managementRoomId)) {
config.managementRoom = await client.joinRoom(config.managementRoom);
} else {
config.managementRoom = managementRoomId;
}

// Switch to using the rule server if we need to
const banLists: BanList[] = [];
if (config.ruleServer?.enabled) {
await logMessage(LogLevel.INFO, "index", "Rule server is starting up.");

// Resolve all the rule server's watched rooms
for (const roomRef of config.ruleServer.listRooms) {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) continue;

let roomId = await client.resolveRoom(permalink.roomIdOrAlias);
if (!joinedRooms.includes(roomId)) {
roomId = await client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
}

banLists.push(new BanList(roomId, roomRef, client));
}

const ruleServer = new RuleServer(client, banLists);
await ruleServer.start();
return;
}

client.on("room.invite", async (roomId: string, inviteEvent: any) => {
const membershipEvent = new MembershipEvent(inviteEvent);

Expand Down Expand Up @@ -86,11 +121,8 @@ if (config.health.healthz.enabled) {
return client.joinRoom(roomId);
});

const banLists: BanList[] = [];
const protectedRooms: { [roomId: string]: string } = {};

const joinedRooms = await client.getJoinedRooms();

// Ensure we're also joined to the rooms we're protecting
LogService.info("index", "Resolving protected rooms...");
for (const roomRef of config.protectedRooms) {
Expand All @@ -105,14 +137,6 @@ if (config.health.healthz.enabled) {
protectedRooms[roomId] = roomRef;
}

// Ensure we're also in the management room
LogService.info("index", "Resolving management room...");
const managementRoomId = await client.resolveRoom(config.managementRoom);
if (!joinedRooms.includes(managementRoomId)) {
config.managementRoom = await client.joinRoom(config.managementRoom);
} else {
config.managementRoom = managementRoomId;
}
await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");

const bot = new Mjolnir(client, protectedRooms, banLists);
Expand Down
2 changes: 1 addition & 1 deletion src/models/ListRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function recommendationToStable(recommendation: string, unstable = true):

export class ListRule {

private glob: MatrixGlob;
public readonly glob: MatrixGlob;

constructor(public readonly entity: string, private action: string, public readonly reason: string, public readonly kind: string) {
this.glob = new MatrixGlob(entity);
Expand Down
Loading