diff --git a/CHANGELOG.md b/CHANGELOG.md
index 779345ee..8f38e98f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@
* make it send pre-recorded messages every X minutes,
* respond message to custom commands.
* many other features will be available in future releases!
+* New settings: "Ban anonymous user's IP when user is banned from a chatroom":
+ * if enabled, every time a streamer bans an anonymous user, it will ban its IP on the chat server,
+ * banned IPs are logged on disk, so server's admin can use them to feed fail2ban (for example),
+ * option disabled by default, because could be used to create trapped-rooms on public servers
### Minor changes and fixes
diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts
index d52e6f9f..db3d1349 100644
--- a/client/admin-plugin-client-plugin.ts
+++ b/client/admin-plugin-client-plugin.ts
@@ -240,6 +240,8 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re
return options.formValues['converse-theme'] !== 'peertube'
case 'chat-per-live-video-warning':
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
+ case 'auto-ban-anonymous-ip':
+ return options.formValues['chat-no-anonymous'] !== false
}
return false
diff --git a/languages/en.yml b/languages/en.yml
index bda2c764..def43704 100644
--- a/languages/en.yml
+++ b/languages/en.yml
@@ -122,6 +122,13 @@ no_anonymous_description: |
If you enabled it, it is highly recommended to also check "Don't publish chat information".
Otherwise, some third party tools could try to open the chat, and have unpredictable behaviours.
+auto_ban_anonymous_ip_label: "Ban anonymous user's IP when user is banned from a chatroom"
+auto_ban_anonymous_ip_description: |
+ By enabling this option, each time an anonymous user is banned from a chatroom, it's IP will also be banned from the chat server.
+ Warning: if your instance is open to registration, any user could create a trapped-room, invite users to join, and automatically ban all anonymous user's IPs.
+ The banned IP list is not stored, it will be cleared on server restart, or when you change some plugin's settings.
+ The banned IP are logged in the Prosody server log files, so server's administrators can eventually use some external tools (like fail2ban) to ban IPs more widely.
+
theming_advanced_description: "
Theming
"
converse_theme_label: "ConverseJS theme"
diff --git a/prosody-modules/mod_muc_ban_ip/README.markdown b/prosody-modules/mod_muc_ban_ip/README.markdown
new file mode 100644
index 00000000..d43956c8
--- /dev/null
+++ b/prosody-modules/mod_muc_ban_ip/README.markdown
@@ -0,0 +1,63 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: Ban users from chatrooms by their IP address
+...
+
+Note: this is a slightly modified version: the log level for IP bans is
+set to info, instead of debug.
+So we can use external tools (fail2ban for example) to block IPs more widely.
+
+Introduction
+============
+
+One frequent complaint about XMPP chatrooms (MUCs) compared to IRC is
+the inability for a room admin to ban a user based on their IP address.
+This is because an XMPP user is not identified on the network by their
+IP address, only their JID.
+
+This means that it is possible to create a new account (usually quite
+easily), and rejoin the room that you were banned from.
+
+This module allows the **user's** server to enforce bans by IP address,
+which is very desirable for server admins who want to prevent their
+server being used for spamming and abusive behaviour.
+
+Details
+=======
+
+An important point to note is that this module enforces the IP ban on
+the banned user's server, not on the MUC server. This means that:
+
+- The user's server MUST have this module loaded, however -
+- The module works even when the MUC is on a different server to the
+ user
+- The MUC server does not need this module (it only needs to support
+ the [standard ban
+ protocol](http://xmpp.org/extensions/xep-0045.html#ban))
+- The module works for effectively banning [anonymous
+ users](http://prosody.im/doc/anonymous_logins)
+
+Also note that IP bans are not saved permanently, and are reset upon a
+server restart.
+
+Configuration
+=============
+
+There is no extra configuration for this module except for loading it.
+Remember... do not load it on the MUC host, simply add it to your global
+`modules_enabled` list, or under a specific host like:
+
+``` lua
+VirtualHost "anon.example.com"
+ authentication = "anonymous"
+ modules_enabled = { "muc_ban_ip" }
+```
+
+Compatibility
+=============
+
+ ----- --------------
+ 0.9 Works
+ 0.8 Doesn't work
+ ----- --------------
diff --git a/prosody-modules/mod_muc_ban_ip/mod_muc_ban_ip.lua b/prosody-modules/mod_muc_ban_ip/mod_muc_ban_ip.lua
new file mode 100644
index 00000000..9dba11e1
--- /dev/null
+++ b/prosody-modules/mod_muc_ban_ip/mod_muc_ban_ip.lua
@@ -0,0 +1,80 @@
+module:set_global();
+
+local jid_bare, jid_host = require "util.jid".bare, require "util.jid".host;
+local st = require "util.stanza";
+local xmlns_muc_user = "http://jabber.org/protocol/muc#user";
+
+local trusted_services = module:get_option_inherited_set("muc_ban_ip_trusted_services", {});
+local trust_local_restricted_services = module:get_option_boolean("muc_ban_ip_trust_local_restricted_services", true);
+
+local ip_bans = module:shared("bans");
+local full_sessions = prosody.full_sessions;
+
+local function is_local_restricted_service(host)
+ local muc_service = prosody.hosts[host] and prosody.hosts[host].modules.muc;
+ if muc_service and module:context(host):get_option("restrict_room_creation") ~= nil then -- COMPAT: May need updating post-0.12
+ return true;
+ end
+ return false;
+end
+
+local function ban_ip(session, from)
+ local ip = session.ip;
+ if not ip then
+ module:log("warn", "Failed to ban IP (IP unknown) for %s", session.full_jid);
+ return;
+ end
+ local from_host = jid_host(from);
+ if trusted_services:contains(from_host) or (trust_local_restricted_services and is_local_restricted_service(from_host)) then
+ from = from_host; -- Ban from entire host
+ end
+ local banned_from = ip_bans[ip];
+ if not banned_from then
+ banned_from = {};
+ ip_bans[ip] = banned_from;
+ end
+ banned_from[from] = true;
+ -- Specific to peertube-plugin-livechat: log level=info.
+ module:log("info", "Added ban for IP address %s from %s", ip, from);
+end
+
+local function check_for_incoming_ban(event)
+ local stanza = event.stanza;
+ local to_session = full_sessions[stanza.attr.to];
+ if to_session then
+ local directed = to_session.directed;
+ local from = stanza.attr.from;
+ if directed and directed[from] and stanza.attr.type == "unavailable" then
+ -- This is a stanza from somewhere we sent directed presence to (may be a MUC)
+ local x = stanza:get_child("x", xmlns_muc_user);
+ if x then
+ for status in x:childtags("status") do
+ if status.attr.code == '301' then
+ ban_ip(to_session, jid_bare(from));
+ end
+ end
+ end
+ end
+ end
+end
+
+local function check_for_ban(event)
+ local origin, stanza = event.origin, event.stanza;
+ local ip = origin.ip;
+ local to, to_host = jid_bare(stanza.attr.to), jid_host(stanza.attr.to);
+ if ip_bans[ip] and (ip_bans[ip][to] or ip_bans[ip][to_host]) then
+ (origin.log or module._log)("debug", "IP banned: %s is banned from %s", ip, to)
+ if stanza.attr.type ~= "error" then
+ origin.send(st.error_reply(stanza, "auth", "forbidden")
+ :tag("x", { xmlns = xmlns_muc_user })
+ :tag("status", { code = '301' }));
+ end
+ return true;
+ end
+ (origin.log or module._log)("debug", "IP not banned: %s from %s", ip, to)
+end
+
+function module.add_host(module)
+ module:hook("presence/full", check_for_incoming_ban, 100);
+ module:hook("pre-presence/full", check_for_ban, 100);
+end
diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts
index cec2ee34..19cb2c42 100644
--- a/server/lib/prosody/config.ts
+++ b/server/lib/prosody/config.ts
@@ -152,6 +152,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise=5.0.0, and is a prerequisite to websocket
diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts
index c99598d0..5bddf818 100644
--- a/server/lib/prosody/config/content.ts
+++ b/server/lib/prosody/config/content.ts
@@ -207,10 +207,13 @@ class ProsodyConfigContent {
this.muc.set('muc_room_default_history_length', 20)
}
- useAnonymous (): void {
+ useAnonymous (autoBanIP: boolean): void {
this.anon = new ProsodyConfigVirtualHost('anon.' + this.prosodyDomain)
this.anon.set('authentication', 'anonymous')
this.anon.set('modules_enabled', ['ping'])
+ if (autoBanIP) {
+ this.anon.add('modules_enabled', 'muc_ban_ip')
+ }
}
useHttpAuthentication (url: string): void {
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index 15a2a8b2..09c6d95e 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -83,7 +83,7 @@ Please read
private: true
})
- // ********** Moderation and advances customization
+ // ********** Advanced channel customization
registerSetting({
type: 'html',
private: true,
@@ -197,6 +197,14 @@ Please read
descriptionHTML: loc('no_anonymous_description'),
private: false
})
+ registerSetting({
+ name: 'auto-ban-anonymous-ip',
+ label: loc('auto_ban_anonymous_ip_label'),
+ type: 'input-checkbox',
+ default: false,
+ descriptionHTML: loc('auto_ban_anonymous_ip_description'),
+ private: true
+ })
// ********** Theming
registerSetting({
diff --git a/support/documentation/content/en/documentation/admin/settings.md b/support/documentation/content/en/documentation/admin/settings.md
index 2672d3ed..b4bed9e2 100644
--- a/support/documentation/content/en/documentation/admin/settings.md
+++ b/support/documentation/content/en/documentation/admin/settings.md
@@ -82,6 +82,10 @@ Note: for now this feature simply hide the chat.
In a future release, the chat will be replaced by a message saying «please log in to [...]».
See [v5.7.0 Release Notes](https://github.com/JohnXLivingston/peertube-plugin-livechat/blob/main/CHANGELOG.md#570) for more information.
+### {{% livechat_label auto_ban_anonymous_ip_label %}}
+
+{{% livechat_label auto_ban_anonymous_ip_description %}}
+
## Theming
### {{% livechat_label converse_theme_label %}}