Skip to content

Managing malicious invites on Matrix with Continuwuity's anti-spam feature

Today, #1263: Add support for invite antispam was merged in the Continuwuity project. So, what does that mean? How does it help mitigate abuse and spam? How does one even set it up? Stop asking questions and start reading.

What is "anti-spam"?

If you've used Synapse before, and experienced spam invites, you may have found yourself asking how you can deal with it going forward. The likely response you got was to set up Draupnir/Meowlnir, and synapse-http-antispam. synapse-http-antispam is a plugin for Synapse that allows the homeserver to ask another service (in this case a moderation bot such as Draupnir or Meowlnir) whether a certain action should be allowed - in most cases, this is "user may invite" and "user may join room".

The "user may join" action checks whether user A is allowed to invite user B to room 1. Typically, the checks performed when the moderation service receives such a request from the homeserver is:

  • Is the user banned by watched policy lists?
  • Is the user's homeserver banned by watched policy lists?
  • Is the room banned by watched policy lists?

And for rooms version 11 and below:

  • Is the server in the room's ID (typically the room creator's server) banned by watched policy lists?

If any of those checks come back positive, the moderation bot will tell synapse-http-antispam in response, which will then tell Synapse to refuse to allow the invite to happen at all. However, if all the checks pass, the moderation service will typically make a note that an invite is pending for the inviteem, which leads us on to our next point:

"user may join room" can be used by the moderation service to do a multitude of things. It can be used to discover rooms that need to have a takedown issued (i.e. room blocked and deleted), or log that a user has joined a room. If a user on the homeserver tries to join a room, Synapse asks anti-spam, which in turn asks the moderation service. If the moderation service detects that the room ID matches a ban policy, it will tell the homeserver to remove the room entirely. It will also track that the user has joined the room, which, if the user was invited to the room, clears the "pending invite" status.

Okay, that means entities that are already banned for abuse won't be able to spread it further. But, what about abuse that got sent out before that ban could be issued? Attackers are known to use scripts to send out hundreds of invites at a time, and obviously someone has to receive the spam before it can be added to policy lists like the [community moderation effort]. Meowlnir has a handy solution to this. When a new ban policy comes in, Meowlnir can scan throuh the list of pending invites it has stored. If a pending invite now matches a new policy, Meowlnir can (using "double puppeting", frequently seen in use by mautrix bridges) act as the affected local user and reject the invite on their behalf. This means abusive invites will be automatically removed from users' clients without them having to do anything manually.

There's also some other neat things you can do with anti-spam, but I'll get to those later.

Enter stage left: Continuwuity anti-spam

As mentioned in the intro, Continuwuity now supports "anti-spam". But it's not via synapse-http-antispam, for reasons you can easily deduce. Instead, Continuwuity went a different route, instead implementing native support for the underlying endpoints that the Synapse plugin calls itself. Not all endpoints were implemented since not all are commonly used, but two important ones ("user may invite" and "user may join room"), and one exotic one ("accept make join") were added. This means you can now enjoy the spoils of anti-spam functionality from the comfort of your Continuwuity deployment.

Accept make join?

Your eyes do not deceive you, a third callback type has entered the mix. "Accept make join" is a custom callback type first implemented in the Maunium Synapse patchset, which has been deployed to the Maunium and Mautrix rooms for months now. This callback allows the server that is helping another server join a room to ask a moderation service whether it should allow the join in the first place. Typically, if the answer would've been a "no", the join would've succeeded anyway, and a moderation bot would've had to manually issue a ban immediately after. This resulted in an ugly chain in rooms where you would see:

--> @abusive:example.com joined the room
--> @modbot:example.org banned @abusive:example.com for spam
--> @modbot:example.org redacted a message from @abusive:example.com

Instead, if the room has an m.room.join_rules that sets the join rules to restricted, and the allow contains one condition: fi.mau.spam_checker, entities that are banned by the moderation service's policies will be forbidden from joining in the first place!

Wait, how does this work?

Since rooms on Matrix are decentralised, sometimes a homeserver will need to ask another one for help with joining a room. "Restricted" rooms are rooms which have join requirements - typically this is "user is a member of a certain space/room", but by nature can be anything.

If the local homeserver is unable to meet the restricted join requirements for a local user itself (e.g. it's never joined the room before, it doesn't have a local user with a high enough power level to issue invites, it can't check the required rooms, etc), it will guess which other servers might be able to help it do so, and will ask them.

In this case, only Continuwuity (and Maunium Synapse) servers will recognise what the special fi.mau.spam_checker requirement means, so every other server will have to ask one of those deployments to help them join new users. You can use this to your advantage by limiting which users can issue invites, allowing you to restrict servers that can provide joins to ones that follow your policy lists.

For example, Continuwuity's community rooms may deploy this rule to only allow our servers that host our moderation bots to issue invites, which ensures that new users trying to join our rooms have to be checked by our policies first. If they fail (e.g. already banned on CME), they won't be able to join our rooms at all, keeping the timeline nice and clean!

How do I deploy it?

First, you need to deploy Draupnir or Meowlnir. That won't be covered here but will eventually be covered in my "setting up continuwuity" guide elsewhere in my blog.

Then, you need to set up invite blocking on the respective bot:

Make sure you take note of the relevant secret token, and then fill in the relevant configuration table in your conduwuit.toml:

[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =

# The authentication secret defined in
# web->synapseHTTPAntispam->authorization
#
#secret =
[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =

# The authentication secret defined in antispam->secret. Required for
# continuwuity to talk to Meowlnir.
#
#secret =

# The management room for which to send requests
#
#management_room =

# If enabled run all federated join attempts (both federated and local)
# through the Meowlnir anti-spam checks.
#
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
# restricted join rule are checked.
#
#check_all_joins = false

For Meowlnir, it is also recommended that you enable notify_management_room, filter_local_invites, and set up auto_reject_invites_token:

# In Meowlnir's configuration file, not continuwuity's:
antispam:
    # Secret used for the synapse-http-antispam API. Same rules apply as for management_secret under meowlnir.
    secret: '1234'
    # If true, Meowlnir will check local invites for spam too instead of only federated ones.
    filter_local_invites: true
    # If set, Meowlnir will use this token to reject pending invites from users who get banned.
    #
    # This should be an appservice with access to all local users. If you have a double puppeting
    # appservice set up for bridges, you can reuse that token. If not, just follow the same
    # instructions: https://docs.mau.fi/bridges/general/double-puppeting.html
    auto_reject_invites_token: abcd
    # Should the management room receive a notice about blocked invites?
    notify_management_room: true
filter_local_invites isn't necessary if you're the sole user of your homeserver, but if you host other people or run bots, you probably want it. Anti-spam will still work without auto_reject_invites_token, but you won't be able to automatically reject invites after new policies, only prevent new ones.

And that's it! Restart your server, and assuming you set everything up right, you should now have working anti-spam.

You can test this out by creating a test user on your server, adding a ban policy for it on a policy list, logging in to it, and send yourself an invite or DM. You should see a message pop up in your management room (or policy notification room for Draupnir), akin to Blocked @test:nexy7574.co.uk from inviting @test:timedout.uk to !FAIkNiwuMi2Z0hAYIF:nexy7574.co.uk due to policy banning @test:nexy7574.co.uk for spam.