7 Commits

Author SHA1 Message Date
MCLV
a6244a9d21 Missing record on creating new in find_entry. Fix for access logic (#30)
Access logic was flawed because if a name existed that was not banned, the IP was not checked for a ban.
2025-11-16 09:47:07 +01:00
MCLV
6bc9b99323 Wildcard (subnet) ban support for IPv4 & IPv6 with trailing * notation (#28)
This enables banning entire subnets or address groups, with documentation and usage examples updated in the README. No changes to command structure or database, all bans work with standard xban commands.
2025-10-28 20:53:29 +01:00
David Leal
e870b8d1d6 Add a license file (#27) 2023-09-11 20:49:22 +02:00
luk3yx
c850d11a3c Use JSON in xban.db (#26)
This should prevent bans database from resetting with a "function has more than 65536 constants" error. Older databases should still be loaded correctly.

Also makes use of minetest.safe_file_write to avoid data corruption.
2023-09-07 17:38:26 +02:00
sfan5
d2cda4f73a Improve behavior of GUI search field 2020-06-02 13:09:52 +02:00
sfan5
37cdbf014e Fix list of names in log message upon (un-)banning 2019-11-05 16:49:50 +01:00
Thomas Rudin
e937f5ff67 Add /xban_cleanup command to purge unbanned entries (#20)
Add documentation for /xban_cleanup
2019-06-06 18:54:46 +02:00
5 changed files with 192 additions and 51 deletions

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2014-2023, Diego Martínez
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,8 +1,5 @@
# Extended Ban Mod for Minetest
This mod attempts to be an improvement to Minetest's ban system.
* It supports normal bans and temporary bans (from 60 seconds up to the end of
time, with 1 second granularity).
* Records and joins all accounts using the same IP address and several IP
@@ -15,26 +12,70 @@ This mod attempts to be an improvement to Minetest's ban system.
* Has an API to ban and check the ban database to allows other mods to manage
users (for example, anticheat mods).
## Chat commands
## Wildcard/Subnet Ban Feature
The mod now supports wildcard (subnet) bans for IP addresses using trailing `*` notation. This allows you to ban entire IP ranges or subnets with a single command.
### How It Works
* **IPv4 Wildcard Bans**: Use a trailing `*` to match any IP address that starts with the specified prefix.
* Example: `192.168.1.*` will match all IPs from `192.168.1.0` to `192.168.1.255`
* Example: `10.0.*` will match all IPs from `10.0.0.0` to `10.0.255.255`
* Example: `172.*` will match all IPs from `172.0.0.0` to `172.255.255.255`
* **IPv6 Wildcard Bans**: Use a trailing `*` to match any IPv6 address that starts with the specified prefix.
* Example: `2001:db8:*` will match all IPv6 addresses starting with `2001:db8:`
* Example: `fe80:*` will match all link-local IPv6 addresses
### Usage Examples
**Ban an entire IPv4 subnet:**
```
/xban 192.168.1.* Banning entire subnet due to spam
```
**Temporarily ban an IPv4 range:**
```
/xtempban 10.0.* 24h Temporary subnet ban for suspected bot activity
```
**Ban an IPv6 prefix:**
```
/xban 2001:db8:* Banning IPv6 prefix
```
**Unban a wildcard entry:**
```
/xunban 192.168.1.*
```
### Notes
* Wildcard bans are checked when a player attempts to connect.
* Individual IP addresses can still be whitelisted even if they match a wildcard ban.
* The wildcard character `*` must be at the end of the IP address.
* For IPv4, you can use wildcards at any octet boundary (e.g., `192.*`, `192.168.*`, `192.168.1.*`).
* For IPv6, the wildcard matches the remaining part of the address after the specified prefix.
## Chat commands
The mod provides the following chat commands. All commands require the `ban`
privilege.
### `xban`
Bans a player permanently.
**Usage:** `/xban <player_or_ip> <reason>`
**Example:** `/xban 127.0.0.1 Some reason.`
### `xtempban`
**Wildcard Example:** `/xban 192.168.1.* Subnet ban`
### `xtempban`
Bans a player temporarily.
**Usage:** `/xtempban <player_or_ip> <time> <reason>`
The `time` parameter is a string in the format `<count><unit>` where `<unit>`
The `time` parameter is a string in the format `<number><unit>` where `<unit>`
is one of `s` for seconds, `m` for minutes, `h` for hours, `D` for days, `W`
for weeks, `M` for months, or `Y` for years. If the unit is omitted, it is
assumed to mean seconds. For example, `42s` means 42 seconds, `1337m` 1337
@@ -43,28 +84,23 @@ up. For example, `1Y3M3D7h` will ban for 1 year, 3 months, 3 days and 7 hours.
**Example:** `/xtempban Joe 3600 Some reason.`
### `xunban`
**Wildcard Example:** `/xtempban 10.0.* 7D Temporary subnet ban`
### `xunban`
Unbans a player.
**Usage:** `/xunban <player_or_ip>`
**Example:** `/xunban Joe`
### `xban_record`
**Wildcard Example:** `/xunban 192.168.1.*`
### `xban_record`
Shows the ban record on chat.
**Usage:** `/xban_record <player_or_ip>`
This prints one ban entry per line, with the time the ban came into effect,
the expiration time (if applicable), the reason, and the source of the ban.
The record is printed to chat with one entry per line.
**Example:** `/xban_record Joe`
### `xban_wl`
Manages the whitelist.
**Usage:** `/xban_wl (add|del|get) <player_or_ip>`
@@ -80,18 +116,15 @@ player is in the whitelist, and prints the status to chat.
**Example:** `/xban_record add Jane`
### `xban_gui`
Shows a form to consult the database interactively.
**Usage:** `/xban_gui`
## Administrator commands
The following commands require the `server` privilege, so they are only
available to server administrators.
### `xban_dbi`
Imports ban entries from other database formats.
**Usage:** `/xban_dbi <importer>`
@@ -104,3 +137,8 @@ the supported import plugins at the time of writing:
* `v2`: Old format used by xban (`players.iplist.v2`).
**Example:** `/xban_dbi minetest`
### `xban_cleanup`
Removes all non-banned entries from the xban db.
**Usage:** `/xban_cleanup`

View File

@@ -61,7 +61,8 @@ local function make_fs(name)
"size[16,12]",
"label[0,-.1;Filter]",
"field[1.5,0;12.8,1;filter;;"..ESC(filter).."]",
"button[14,-.3;2,1;search;Search]",
"field_close_on_enter[filter;false]",
"button[14,-.3;2,1;search_submit;Search]",
}
local fsn = #fs
fsn=fsn+1 fs[fsn] = format("textlist[0,.8;4,9.3;player;%s;%d;0]",
@@ -122,7 +123,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
return
end
if fields.search then
if fields.search_submit or fields.filter then
local filter = fields.filter or ""
state.filter = filter
state.list = make_list(filter)

View File

@@ -25,6 +25,7 @@ end
local ACTION = make_logger("action")
local WARNING = make_logger("warning")
local ERROR = make_logger("error")
local unit_to_secs = {
s = 1, m = 60, h = 3600,
@@ -40,23 +41,39 @@ local function parse_time(t) --> secs
return secs
end
function xban.find_entry(player, create) --> entry, index
for index, e in ipairs(db) do
local function concat_keys(t, sep)
local keys = {}
for k, _ in pairs(t) do
keys[#keys + 1] = k
end
return table.concat(keys, sep)
end
-- supports wildcard IP pattern (both IPv4 and IPv6)
function xban.find_entry(key, create)
-- exact match (player or IP)
for i, e in ipairs(xban.db) do
if e.names[key] then return e, i end
end
-- wildcard pattern match for IPs
if key and key:find("[.:]") then
for i, e in ipairs(xban.db) do
for name in pairs(e.names) do
if name == player then
return e, index
local wildcard_prefix = name:match("(.+[.:])%*$")
if wildcard_prefix and key:sub(1, #wildcard_prefix) == wildcard_prefix then
return e, i
end
end
end
end
if create then
print(("Created new entry for `%s'"):format(player))
local e = {
names = { [player]=true },
names = { [key]=true },
banned = false,
record = { },
}
table.insert(db, e)
return e, #db
table.insert(xban.db, e)
return e, #xban.db
end
return nil
end
@@ -111,7 +128,7 @@ function xban.ban_player(player, source, expires, reason) --> bool, err
end
ACTION("%s bans %s until %s for reason: %s", source, player,
date, reason)
ACTION("Banned Names/IPs: %s", table.concat(e.names, ", "))
ACTION("Banned Names/IPs: %s", concat_keys(e.names, ", "))
return true
end
@@ -131,7 +148,7 @@ function xban.unban_player(player, source) --> bool, err
e.expires = nil
e.time = nil
ACTION("%s unbans %s", source, player)
ACTION("Unbanned Names/IPs: %s", table.concat(e.names, ", "))
ACTION("Unbanned Names/IPs: %s", concat_keys(e.names, ", "))
return true
end
@@ -186,13 +203,16 @@ end
minetest.register_on_prejoinplayer(function(name, ip)
local wl = db.whitelist or {}
if wl[name] or wl[ip] then return end
local e = xban.find_entry(name) or xban.find_entry(ip)
if not e then return end
if e.banned then
local date = (e.expires and os.date("%c", e.expires)
or "the end of time")
return ("Banned: Expires: %s, Reason: %s"):format(
date, e.reason)
local e = xban.find_entry(name)
if not e or not e.banned then
e = ip and xban.find_entry(ip)
end
if e and e.banned then
local date = e.expires and os.date("%c", e.expires) or "the end of time"
local reason = e.reason or "No reason given"
return ("Banned: Expires: %s, Reason: %s"):format(date, reason)
end
end)
@@ -314,6 +334,7 @@ minetest.register_chatcommand("xban_wl", {
end,
})
local function check_temp_bans()
minetest.after(60, check_temp_bans)
local to_rm = { }
@@ -334,18 +355,12 @@ end
local function save_db()
minetest.after(SAVE_INTERVAL, save_db)
local f, e = io.open(DB_FILENAME, "wt")
db.timestamp = os.time()
if f then
local ok, err = f:write(xban.serialize(db))
local contents = assert(xban.serialize_db(db))
local ok = minetest.safe_file_write(DB_FILENAME, contents)
if not ok then
WARNING("Unable to save database: %s", err)
ERROR("Unable to save database")
end
else
WARNING("Unable to save database: %s", e)
end
if f then f:close() end
return
end
local function load_db()
@@ -359,7 +374,7 @@ local function load_db()
WARNING("Unable to load database: %s", "Read failed")
return
end
local t, e2 = minetest.deserialize(cont)
local t, e2 = xban.deserialize_db(cont)
if not t then
WARNING("Unable to load database: %s",
"Deserialization failed: "..(e2 or "unknown error"))
@@ -374,6 +389,30 @@ local function load_db()
end
end
minetest.register_chatcommand("xban_cleanup", {
description = "Removes all non-banned entries from the xban db",
privs = { server=true },
func = function(name, params)
local old_count = #db
local i = 1
while i <= #db do
if not db[i].banned then
-- not banned, remove from db
table.remove(db, i)
else
-- banned, hold entry back
i = i + 1
end
end
-- save immediately
save_db()
return true, "Removed " .. (old_count - #db) .. " entries, new db entry-count: " .. #db
end,
})
minetest.register_on_shutdown(save_db)
minetest.after(SAVE_INTERVAL, save_db)
load_db()

View File

@@ -27,5 +27,44 @@ local function my_serialize_2(t, level)
end
function xban.serialize(t)
minetest.log("warning", "[xban2] xban.serialize() is deprecated")
return "return {\n"..my_serialize_2(t, 1).."\n}"
end
-- JSON doesn't allow combined string+number keys, this function moves any
-- number keys into an "entries" table
function xban.serialize_db(t)
local res = {}
local entries = {}
for k, v in pairs(t) do
if type(k) == "number" then
entries[k] = v
else
res[k] = v
end
end
res.entries = entries
return minetest.write_json(res, true)
end
function xban.deserialize_db(s)
if s:sub(1, 1) ~= "{" then
-- Load legacy databases
return minetest.deserialize(s)
end
local res, err = minetest.parse_json(s)
if not res then
return nil, err
end
-- Remove all "null"s added by empty tables
for i, entry in ipairs(res.entries or {}) do
entry.names = entry.names or {}
entry.record = entry.record or {}
res[i] = entry
end
res.entries = nil
return res
end