From 08b260a9db7599554164d56c9056d9dc199b6fd6 Mon Sep 17 00:00:00 2001 From: yello Date: Tue, 14 Jul 2026 19:32:11 +0200 Subject: [PATCH 1/7] Enforce whitelist during connection handshake --- src/network/protocols.zig | 5 +++++ src/server/world.zig | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 50a7350942..9775499bd8 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -171,6 +171,11 @@ pub const handShake = struct { // MARK: handShake const keys = zon.getChild("keys"); try conn.user.?.identifyFromKeysAndName(name, keys); + if (main.server.world.?.settings.whitelistEnabled and !main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?)) { + std.log.info("Rejected connection from '{s}': not on whitelist", .{name}); + return error.NotWhitelisted; + } + var writer: utils.BinaryWriter = .init(main.stackAllocator); defer writer.deinit(); writer.writeEnum(Connection.HandShakeState, .signatureRequest); diff --git a/src/server/world.zig b/src/server/world.zig index 2aba5ac0b0..e795b88870 100644 --- a/src/server/world.zig +++ b/src/server/world.zig @@ -35,6 +35,7 @@ pub const Settings = struct { defaultGamemode: Gamemode = .creative, allowCheats: bool = true, testingMode: bool = false, + whitelistEnabled: bool = false, seed: u64 = undefined, pub const defaults: Settings = .{}; @@ -48,6 +49,7 @@ pub const Settings = struct { .defaultGamemode = std.meta.stringToEnum(main.game.Gamemode, zon.get([]const u8, "defaultGamemode") orelse @tagName(defaults.defaultGamemode)) orelse defaults.defaultGamemode, .allowCheats = zon.get(bool, "allowCheats") orelse defaults.allowCheats, .testingMode = zon.get(bool, "testingMode") orelse defaults.testingMode, + .whitelistEnabled = zon.get(bool, "whitelistEnabled") orelse defaults.whitelistEnabled, }; } @@ -57,6 +59,7 @@ pub const Settings = struct { zon.put("defaultGamemode", @tagName(self.defaultGamemode)); zon.put("allowCheats", self.allowCheats); zon.put("testingMode", self.testingMode); + zon.put("whitelistEnabled", self.whitelistEnabled); zon.put("seed", self.seed); return zon; From 97f1729e6ee296b9d7eeb2fb539fd85a0fe60635 Mon Sep 17 00:00:00 2001 From: yello Date: Tue, 14 Jul 2026 19:32:30 +0200 Subject: [PATCH 2/7] Add /whitelist command --- src/network.zig | 13 +++++-- src/server/command/_list.zig | 1 + src/server/command/whitelist.zig | 64 ++++++++++++++++++++++++++++++++ src/server/players.zig | 1 - 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/server/command/whitelist.zig diff --git a/src/network.zig b/src/network.zig index 7b6477bf4e..6b41cda54b 100644 --- a/src/network.zig +++ b/src/network.zig @@ -1737,11 +1737,16 @@ pub const Connection = struct { // MARK: Connection pub fn receive(self: *Connection, data: []const u8) void { self.tryReceive(data) catch |err| { - std.log.err("Got error while processing received network data: {s}", .{@errorName(err)}); - if (@errorReturnTrace()) |trace| { - std.log.info("{f}", .{main.fmt.FormatErrorTrace{.stackTrace = trace.*}}); + switch (err) { + error.NotWhitelisted => {}, + else => { + std.log.err("Got error while processing received network data: {s}", .{@errorName(err)}); + if (@errorReturnTrace()) |trace| { + std.log.info("{f}", .{main.fmt.FormatErrorTrace{.stackTrace = trace.*}}); + } + std.log.debug("Packet data: {any}", .{data}); + }, } - std.log.debug("Packet data: {any}", .{data}); self.disconnect(); }; } diff --git a/src/server/command/_list.zig b/src/server/command/_list.zig index 00b2dd1f4e..a4e0e44980 100644 --- a/src/server/command/_list.zig +++ b/src/server/command/_list.zig @@ -11,6 +11,7 @@ pub const spawn = @import("spawn.zig"); pub const tickspeed = @import("tickspeed.zig"); pub const time = @import("time.zig"); pub const tp = @import("tp.zig"); +pub const whitelist = @import("whitelist.zig"); pub const avatar = @import("entity/avatar.zig"); diff --git a/src/server/command/whitelist.zig b/src/server/command/whitelist.zig new file mode 100644 index 0000000000..283e6c8393 --- /dev/null +++ b/src/server/command/whitelist.zig @@ -0,0 +1,64 @@ +const std = @import("std"); + +const main = @import("main"); +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const ListManaged = main.ListManaged; +const command = main.server.command; +const Source = command.Source; +const players = main.server.players; + +pub const description = "Manages the connection whitelist"; +pub const usage = + \\/whitelist : + \\/whitelist @ +; + +const Action = enum { add, block }; + +pub const Args = union(enum) { + @"/whitelist ": struct { action: Action, key: KeyString }, + @"/whitelist ": struct { action: Action, playerIndex: command.PlayerIndex }, +}; + +pub fn execute(args: Args, source: Source) void { + switch (args) { + .@"/whitelist " => |params| applyAction(source, params.action, params.key.key), + .@"/whitelist " => |params| { + const target = command.Target.fromPlayerIndex(params.playerIndex, source) catch return; + const key = target.user.newKeyString orelse { + source.sendMessage("#ff0000Player {s}§#ff0000 has no public key to whitelist", .{target.user.name}); + return; + }; + applyAction(source, params.action, key); + }, + } +} + +fn applyAction(source: Source, action: Action, key: []const u8) void { + switch (action) { + .add => switch (players.add(key)) { + .added => source.sendMessage("#00ff00Added {s}§#00ff00 to the whitelist", .{key}), + .alreadyAllowed => source.sendMessage("#ff0000{s}§#ff0000 is already on the whitelist", .{key}), + }, + .block => switch (players.block(key)) { + .blocked => source.sendMessage("#00ff00Blocked {s}§#00ff00 from connecting", .{key}), + .alreadyBlocked => source.sendMessage("#ff0000{s}§#ff0000 is already blocked", .{key}), + }, + } +} + +const KeyString = struct { + key: []const u8, + + pub fn parse(_: NeverFailingAllocator, name: []const u8, arg: []const u8, errorMessage: *ListManaged(u8)) error{ParseError}!KeyString { + const colonIndex = std.mem.indexOfScalar(u8, arg, ':') orelse { + errorMessage.print("Expected a public key of the form \":\" for <{s}>, found \"{s}\"", .{name, arg}); + return error.ParseError; + }; + _ = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse { + errorMessage.print("Unknown key type \"{s}\" for <{s}>", .{arg[0..colonIndex], name}); + return error.ParseError; + }; + return .{.key = arg}; + } +}; diff --git a/src/server/players.zig b/src/server/players.zig index a45051674c..437b700c10 100644 --- a/src/server/players.zig +++ b/src/server/players.zig @@ -186,7 +186,6 @@ pub fn block(key: []const u8) BlockResult { } pub fn isAllowedToJoin(key: []const u8) bool { - sync.threadContext.assertCorrectContext(.server); mutex.lock(); defer mutex.unlock(); const entry = playerDatabase.get(key) orelse return false; From 12b960bc13d678cd2f3474243d4401c1767eb2a0 Mon Sep 17 00:00:00 2001 From: yello Date: Tue, 4 Aug 2026 22:26:49 +0200 Subject: [PATCH 3/7] Decouple ban from whitelist --- src/network/protocols.zig | 13 ++++++++++--- src/server/command.zig | 16 ++++++++++++++++ src/server/command/whitelist.zig | 31 +++++++++---------------------- src/server/players.zig | 28 +++++++++++++++------------- src/server/server.zig | 11 +++++++++++ 5 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 9775499bd8..a111a5af5d 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -171,9 +171,16 @@ pub const handShake = struct { // MARK: handShake const keys = zon.getChild("keys"); try conn.user.?.identifyFromKeysAndName(name, keys); - if (main.server.world.?.settings.whitelistEnabled and !main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?)) { - std.log.info("Rejected connection from '{s}': not on whitelist", .{name}); - return error.NotWhitelisted; + switch (main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?)) { + .allowed => {}, + .blocked => { + std.log.info("Rejected connection from '{s}': blocked", .{name}); + return error.NotWhitelisted; + }, + .neutral => if (main.server.world.?.settings.whitelistEnabled) { + std.log.info("Rejected connection from '{s}': not on whitelist", .{name}); + return error.NotWhitelisted; + }, } var writer: utils.BinaryWriter = .init(main.stackAllocator); diff --git a/src/server/command.zig b/src/server/command.zig index 1acdc5472e..b51978be02 100644 --- a/src/server/command.zig +++ b/src/server/command.zig @@ -169,6 +169,22 @@ pub const PlayerIndex = struct { } }; +pub const KeyString = struct { + key: []const u8, + + pub fn parse(_: NeverFailingAllocator, name: []const u8, arg: []const u8, errorMessage: *ListManaged(u8)) error{ParseError}!KeyString { + const colonIndex = std.mem.indexOfScalar(u8, arg, ':') orelse { + errorMessage.print("Expected a public key of the form \":\" for <{s}>, found \"{s}\"", .{name, arg}); + return error.ParseError; + }; + _ = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse { + errorMessage.print("Unknown key type \"{s}\" for <{s}>", .{arg[0..colonIndex], name}); + return error.ParseError; + }; + return .{.key = arg}; + } +}; + pub const BiomeId = struct { biome: *const main.server.terrain.biomes.Biome, diff --git a/src/server/command/whitelist.zig b/src/server/command/whitelist.zig index 283e6c8393..c00448941e 100644 --- a/src/server/command/whitelist.zig +++ b/src/server/command/whitelist.zig @@ -1,8 +1,6 @@ const std = @import("std"); const main = @import("main"); -const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const ListManaged = main.ListManaged; const command = main.server.command; const Source = command.Source; const players = main.server.players; @@ -16,7 +14,7 @@ pub const usage = const Action = enum { add, block }; pub const Args = union(enum) { - @"/whitelist ": struct { action: Action, key: KeyString }, + @"/whitelist ": struct { action: Action, key: command.KeyString }, @"/whitelist ": struct { action: Action, playerIndex: command.PlayerIndex }, }; @@ -40,25 +38,14 @@ fn applyAction(source: Source, action: Action, key: []const u8) void { .added => source.sendMessage("#00ff00Added {s}§#00ff00 to the whitelist", .{key}), .alreadyAllowed => source.sendMessage("#ff0000{s}§#ff0000 is already on the whitelist", .{key}), }, - .block => switch (players.block(key)) { - .blocked => source.sendMessage("#00ff00Blocked {s}§#00ff00 from connecting", .{key}), - .alreadyBlocked => source.sendMessage("#ff0000{s}§#ff0000 is already blocked", .{key}), + .block => { + switch (players.block(key)) { + .blocked => source.sendMessage("#00ff00Blocked {s}§#00ff00 from connecting", .{key}), + .alreadyBlocked => source.sendMessage("#ff0000{s}§#ff0000 is already blocked", .{key}), + } + if (main.server.getUserByKey(key)) |user| { + user.conn.disconnect(); + } }, } } - -const KeyString = struct { - key: []const u8, - - pub fn parse(_: NeverFailingAllocator, name: []const u8, arg: []const u8, errorMessage: *ListManaged(u8)) error{ParseError}!KeyString { - const colonIndex = std.mem.indexOfScalar(u8, arg, ':') orelse { - errorMessage.print("Expected a public key of the form \":\" for <{s}>, found \"{s}\"", .{name, arg}); - return error.ParseError; - }; - _ = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse { - errorMessage.print("Unknown key type \"{s}\" for <{s}>", .{arg[0..colonIndex], name}); - return error.ParseError; - }; - return .{.key = arg}; - } -}; diff --git a/src/server/players.zig b/src/server/players.zig index 437b700c10..fea3af9ef9 100644 --- a/src/server/players.zig +++ b/src/server/players.zig @@ -185,11 +185,13 @@ pub fn block(key: []const u8) BlockResult { return if (result.wasNew or !wasBlocked) .blocked else .alreadyBlocked; } -pub fn isAllowedToJoin(key: []const u8) bool { +pub const JoinResult = enum { allowed, neutral, blocked }; + +pub fn isAllowedToJoin(key: []const u8) JoinResult { mutex.lock(); defer mutex.unlock(); - const entry = playerDatabase.get(key) orelse return false; - return !entry.blocked; + const entry = playerDatabase.get(key) orelse return .neutral; + return if (entry.blocked) .blocked else .allowed; } test "addContainsRemove" { @@ -198,13 +200,13 @@ test "addContainsRemove" { init("test", 0); - try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(.neutral, isAllowedToJoin("ed25519:abc")); try std.testing.expectEqual(.added, add("ed25519:abc")); try std.testing.expectEqual(.alreadyAllowed, add("ed25519:abc")); - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:abc")); try std.testing.expectEqual(.blocked, block("ed25519:abc")); try std.testing.expectEqual(.alreadyBlocked, block("ed25519:abc")); - try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:abc")); } test "addUnblocks" { @@ -214,12 +216,12 @@ test "addUnblocks" { init("test", 0); try std.testing.expectEqual(.blocked, block("ed25519:xyz")); - try std.testing.expectEqual(false, isAllowedToJoin("ed25519:xyz")); + try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:xyz")); try std.testing.expectEqual(.added, add("ed25519:xyz")); - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz")); + try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:xyz")); } -test "knownPlayerAllowedByDefaultButBlockable" { +test "allowedNeutralAndBlockedStates" { main.heap.allocators.createWorldArena(); defer main.heap.allocators.destroyWorldArena(); @@ -227,12 +229,12 @@ test "knownPlayerAllowedByDefaultButBlockable" { playerDatabase.put(main.worldArena.allocator, main.worldArena.dupe(u8, "ed25519:known"), .{.playerIndex = 0, .blocked = false}) catch unreachable; - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known")); - try std.testing.expectEqual(false, isAllowedToJoin("ed25519:unknown")); + try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(.neutral, isAllowedToJoin("ed25519:unknown")); try std.testing.expectEqual(.blocked, block("ed25519:known")); - try std.testing.expectEqual(false, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:known")); try std.testing.expectEqual(.added, add("ed25519:known")); - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:known")); } diff --git a/src/server/server.zig b/src/server/server.zig index 4860d956cd..6fed676243 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -891,3 +891,14 @@ pub fn getUserByIndex(index: PlayerIndex) ?*User { } return null; } + +pub fn getUserByKey(key: []const u8) ?*User { + const userList = getUserList(main.stackAllocator); + defer main.stackAllocator.free(userList); + for (userList) |user| { + if (user.newKeyString) |userKey| { + if (std.mem.eql(u8, userKey, key)) return user; + } + } + return null; +} From 7d6edbcbc4831eb037825cf703a256fc3fe6db34 Mon Sep 17 00:00:00 2001 From: yello Date: Tue, 4 Aug 2026 22:32:52 +0200 Subject: [PATCH 4/7] Make JoinResult not pub --- src/server/players.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/players.zig b/src/server/players.zig index fea3af9ef9..ba26a65e2e 100644 --- a/src/server/players.zig +++ b/src/server/players.zig @@ -185,7 +185,7 @@ pub fn block(key: []const u8) BlockResult { return if (result.wasNew or !wasBlocked) .blocked else .alreadyBlocked; } -pub const JoinResult = enum { allowed, neutral, blocked }; +const JoinResult = enum { allowed, neutral, blocked }; pub fn isAllowedToJoin(key: []const u8) JoinResult { mutex.lock(); From 49bd0fd3b0a456955d7c3635493c737f3d0ee6d3 Mon Sep 17 00:00:00 2001 From: yello Date: Fri, 7 Aug 2026 16:04:38 +0200 Subject: [PATCH 5/7] Assert context on add and block --- src/server/players.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/players.zig b/src/server/players.zig index ba26a65e2e..5d4a095e47 100644 --- a/src/server/players.zig +++ b/src/server/players.zig @@ -164,6 +164,7 @@ fn saveBlocked(index: usize, value: bool) void { const AddResult = enum { added, alreadyAllowed }; pub fn add(key: []const u8) AddResult { + sync.threadContext.assertCorrectContext(.server); mutex.lock(); defer mutex.unlock(); const result = ensurePlayerRecord(key); @@ -176,6 +177,7 @@ pub fn add(key: []const u8) AddResult { const BlockResult = enum { blocked, alreadyBlocked }; pub fn block(key: []const u8) BlockResult { + sync.threadContext.assertCorrectContext(.server); mutex.lock(); defer mutex.unlock(); const result = ensurePlayerRecord(key); From a3d57b89c0b2f3e0d0c8b3178e5e37d03d508f99 Mon Sep 17 00:00:00 2001 From: yello Date: Sat, 8 Aug 2026 16:26:37 +0200 Subject: [PATCH 6/7] Log warning and add toggle command --- src/network.zig | 13 ++++--------- src/network/protocols.zig | 13 +++---------- src/server/command.zig | 6 +++++- src/server/command/whitelist.zig | 18 ++++++++++++++++-- src/server/players.zig | 29 ++++++++++++++--------------- src/server/server.zig | 11 ----------- src/server/world.zig | 6 +++--- 7 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/network.zig b/src/network.zig index 6b41cda54b..8fc4321a05 100644 --- a/src/network.zig +++ b/src/network.zig @@ -1737,16 +1737,11 @@ pub const Connection = struct { // MARK: Connection pub fn receive(self: *Connection, data: []const u8) void { self.tryReceive(data) catch |err| { - switch (err) { - error.NotWhitelisted => {}, - else => { - std.log.err("Got error while processing received network data: {s}", .{@errorName(err)}); - if (@errorReturnTrace()) |trace| { - std.log.info("{f}", .{main.fmt.FormatErrorTrace{.stackTrace = trace.*}}); - } - std.log.debug("Packet data: {any}", .{data}); - }, + std.log.warn("Got error while processing received network data: {s}", .{@errorName(err)}); + if (@errorReturnTrace()) |trace| { + std.log.info("{f}", .{main.fmt.FormatErrorTrace{.stackTrace = trace.*}}); } + std.log.debug("Packet data: {any}", .{data}); self.disconnect(); }; } diff --git a/src/network/protocols.zig b/src/network/protocols.zig index a111a5af5d..454ec886c7 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -171,16 +171,9 @@ pub const handShake = struct { // MARK: handShake const keys = zon.getChild("keys"); try conn.user.?.identifyFromKeysAndName(name, keys); - switch (main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?)) { - .allowed => {}, - .blocked => { - std.log.info("Rejected connection from '{s}': blocked", .{name}); - return error.NotWhitelisted; - }, - .neutral => if (main.server.world.?.settings.whitelistEnabled) { - std.log.info("Rejected connection from '{s}': not on whitelist", .{name}); - return error.NotWhitelisted; - }, + if (!main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?, main.server.world.?.whitelistEnabled.load(.monotonic))) { + std.log.info("Rejected connection from '{s}' ({s})", .{name, conn.user.?.newKeyString.?}); + return error.NotWhitelisted; } var writer: utils.BinaryWriter = .init(main.stackAllocator); diff --git a/src/server/command.zig b/src/server/command.zig index b51978be02..d2bcc70a04 100644 --- a/src/server/command.zig +++ b/src/server/command.zig @@ -177,10 +177,14 @@ pub const KeyString = struct { errorMessage.print("Expected a public key of the form \":\" for <{s}>, found \"{s}\"", .{name, arg}); return error.ParseError; }; - _ = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse { + const keyType = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse { errorMessage.print("Unknown key type \"{s}\" for <{s}>", .{arg[0..colonIndex], name}); return error.ParseError; }; + _ = main.network.authentication.PublicKey.initFromBase64(arg[colonIndex + 1 ..], keyType) catch { + errorMessage.print("Invalid public key \"{s}\" for <{s}>", .{arg, name}); + return error.ParseError; + }; return .{.key = arg}; } }; diff --git a/src/server/command/whitelist.zig b/src/server/command/whitelist.zig index c00448941e..11b8423957 100644 --- a/src/server/command/whitelist.zig +++ b/src/server/command/whitelist.zig @@ -9,13 +9,16 @@ pub const description = "Manages the connection whitelist"; pub const usage = \\/whitelist : \\/whitelist @ + \\/whitelist ; const Action = enum { add, block }; +const Toggle = enum { enable, disable }; pub const Args = union(enum) { @"/whitelist ": struct { action: Action, key: command.KeyString }, @"/whitelist ": struct { action: Action, playerIndex: command.PlayerIndex }, + @"/whitelist ": struct { toggle: Toggle }, }; pub fn execute(args: Args, source: Source) void { @@ -29,6 +32,10 @@ pub fn execute(args: Args, source: Source) void { }; applyAction(source, params.action, key); }, + .@"/whitelist " => |params| { + main.server.world.?.whitelistEnabled.store(params.toggle == .enable, .monotonic); + source.sendMessage("#00ff00Whitelist {s}", .{if (params.toggle == .enable) "enabled" else "disabled"}); + }, } } @@ -43,8 +50,15 @@ fn applyAction(source: Source, action: Action, key: []const u8) void { .blocked => source.sendMessage("#00ff00Blocked {s}§#00ff00 from connecting", .{key}), .alreadyBlocked => source.sendMessage("#ff0000{s}§#ff0000 is already blocked", .{key}), } - if (main.server.getUserByKey(key)) |user| { - user.conn.disconnect(); + const userList = main.server.getUserList(main.stackAllocator); + defer main.stackAllocator.free(userList); + for (userList) |user| { + if (user.newKeyString) |userKey| { + if (std.mem.eql(u8, userKey, key)) { + user.conn.disconnect(); + break; + } + } } }, } diff --git a/src/server/players.zig b/src/server/players.zig index 5d4a095e47..24b8d3d374 100644 --- a/src/server/players.zig +++ b/src/server/players.zig @@ -187,13 +187,11 @@ pub fn block(key: []const u8) BlockResult { return if (result.wasNew or !wasBlocked) .blocked else .alreadyBlocked; } -const JoinResult = enum { allowed, neutral, blocked }; - -pub fn isAllowedToJoin(key: []const u8) JoinResult { +pub fn isAllowedToJoin(key: []const u8, whitelistEnabled: bool) bool { mutex.lock(); defer mutex.unlock(); - const entry = playerDatabase.get(key) orelse return .neutral; - return if (entry.blocked) .blocked else .allowed; + const entry = playerDatabase.get(key) orelse return !whitelistEnabled; + return !entry.blocked; } test "addContainsRemove" { @@ -202,13 +200,13 @@ test "addContainsRemove" { init("test", 0); - try std.testing.expectEqual(.neutral, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc", true)); try std.testing.expectEqual(.added, add("ed25519:abc")); try std.testing.expectEqual(.alreadyAllowed, add("ed25519:abc")); - try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:abc", true)); try std.testing.expectEqual(.blocked, block("ed25519:abc")); try std.testing.expectEqual(.alreadyBlocked, block("ed25519:abc")); - try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc", true)); } test "addUnblocks" { @@ -218,12 +216,12 @@ test "addUnblocks" { init("test", 0); try std.testing.expectEqual(.blocked, block("ed25519:xyz")); - try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:xyz")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:xyz", false)); try std.testing.expectEqual(.added, add("ed25519:xyz")); - try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:xyz")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz", false)); } -test "allowedNeutralAndBlockedStates" { +test "whitelistToggleAffectsUnknownKeysOnly" { main.heap.allocators.createWorldArena(); defer main.heap.allocators.destroyWorldArena(); @@ -231,12 +229,13 @@ test "allowedNeutralAndBlockedStates" { playerDatabase.put(main.worldArena.allocator, main.worldArena.dupe(u8, "ed25519:known"), .{.playerIndex = 0, .blocked = false}) catch unreachable; - try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:known")); - try std.testing.expectEqual(.neutral, isAllowedToJoin("ed25519:unknown")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known", true)); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:unknown", false)); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:unknown", true)); try std.testing.expectEqual(.blocked, block("ed25519:known")); - try std.testing.expectEqual(.blocked, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:known", false)); try std.testing.expectEqual(.added, add("ed25519:known")); - try std.testing.expectEqual(.allowed, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known", false)); } diff --git a/src/server/server.zig b/src/server/server.zig index 6fed676243..4860d956cd 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -891,14 +891,3 @@ pub fn getUserByIndex(index: PlayerIndex) ?*User { } return null; } - -pub fn getUserByKey(key: []const u8) ?*User { - const userList = getUserList(main.stackAllocator); - defer main.stackAllocator.free(userList); - for (userList) |user| { - if (user.newKeyString) |userKey| { - if (std.mem.eql(u8, userKey, key)) return user; - } - } - return null; -} diff --git a/src/server/world.zig b/src/server/world.zig index e795b88870..32ec748083 100644 --- a/src/server/world.zig +++ b/src/server/world.zig @@ -35,7 +35,6 @@ pub const Settings = struct { defaultGamemode: Gamemode = .creative, allowCheats: bool = true, testingMode: bool = false, - whitelistEnabled: bool = false, seed: u64 = undefined, pub const defaults: Settings = .{}; @@ -49,7 +48,6 @@ pub const Settings = struct { .defaultGamemode = std.meta.stringToEnum(main.game.Gamemode, zon.get([]const u8, "defaultGamemode") orelse @tagName(defaults.defaultGamemode)) orelse defaults.defaultGamemode, .allowCheats = zon.get(bool, "allowCheats") orelse defaults.allowCheats, .testingMode = zon.get(bool, "testingMode") orelse defaults.testingMode, - .whitelistEnabled = zon.get(bool, "whitelistEnabled") orelse defaults.whitelistEnabled, }; } @@ -59,7 +57,6 @@ pub const Settings = struct { zon.put("defaultGamemode", @tagName(self.defaultGamemode)); zon.put("allowCheats", self.allowCheats); zon.put("testingMode", self.testingMode); - zon.put("whitelistEnabled", self.whitelistEnabled); zon.put("seed", self.seed); return zon; @@ -446,6 +443,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld doGameTimeCycle: bool = true, tickSpeed: std.atomic.Value(u32) = .init(12), + whitelistEnabled: std.atomic.Value(bool) = .init(false), settings: Settings = undefined, @@ -653,6 +651,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld self.biomeChecksum = worldData.get(i64, "biomeChecksum") orelse 0; self.name = main.globalAllocator.dupe(u8, worldData.get([]const u8, "name") orelse self.path); self.tickSpeed = .init(worldData.get(u32, "tickSpeed") orelse 12); + self.whitelistEnabled = .init(worldData.get(bool, "whitelistEnabled") orelse false); } pub fn saveWorldConfig(self: *ServerWorld) !void { @@ -668,6 +667,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld worldData.put("name", self.name); worldData.put("lastUsedTime", std.Io.Clock.Timestamp.now(main.io, .real).raw.toMilliseconds()); worldData.put("tickSpeed", self.tickSpeed.load(.monotonic)); + worldData.put("whitelistEnabled", self.whitelistEnabled.load(.monotonic)); worldData.put("localPlayer", players.getLocalPlayerIndex()); try files.cubyzDir().writeZon(path, worldData); From 4ded5abf95cd636e96f161dfdd5eb2c7ab9813d9 Mon Sep 17 00:00:00 2001 From: yello Date: Sat, 8 Aug 2026 18:29:52 +0200 Subject: [PATCH 7/7] Persist whitelistEnabled through Settings on every save --- src/network/protocols.zig | 2 +- src/server/command/whitelist.zig | 2 +- src/server/world.zig | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 454ec886c7..d8c4db7be0 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -171,7 +171,7 @@ pub const handShake = struct { // MARK: handShake const keys = zon.getChild("keys"); try conn.user.?.identifyFromKeysAndName(name, keys); - if (!main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?, main.server.world.?.whitelistEnabled.load(.monotonic))) { + if (!main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?, main.server.world.?.settings.whitelistEnabled.load(.monotonic))) { std.log.info("Rejected connection from '{s}' ({s})", .{name, conn.user.?.newKeyString.?}); return error.NotWhitelisted; } diff --git a/src/server/command/whitelist.zig b/src/server/command/whitelist.zig index 11b8423957..c8e96e83a7 100644 --- a/src/server/command/whitelist.zig +++ b/src/server/command/whitelist.zig @@ -33,7 +33,7 @@ pub fn execute(args: Args, source: Source) void { applyAction(source, params.action, key); }, .@"/whitelist " => |params| { - main.server.world.?.whitelistEnabled.store(params.toggle == .enable, .monotonic); + main.server.world.?.settings.whitelistEnabled.store(params.toggle == .enable, .monotonic); source.sendMessage("#00ff00Whitelist {s}", .{if (params.toggle == .enable) "enabled" else "disabled"}); }, } diff --git a/src/server/world.zig b/src/server/world.zig index 32ec748083..ba3bf1c9de 100644 --- a/src/server/world.zig +++ b/src/server/world.zig @@ -35,6 +35,7 @@ pub const Settings = struct { defaultGamemode: Gamemode = .creative, allowCheats: bool = true, testingMode: bool = false, + whitelistEnabled: std.atomic.Value(bool) = .init(false), seed: u64 = undefined, pub const defaults: Settings = .{}; @@ -48,15 +49,17 @@ pub const Settings = struct { .defaultGamemode = std.meta.stringToEnum(main.game.Gamemode, zon.get([]const u8, "defaultGamemode") orelse @tagName(defaults.defaultGamemode)) orelse defaults.defaultGamemode, .allowCheats = zon.get(bool, "allowCheats") orelse defaults.allowCheats, .testingMode = zon.get(bool, "testingMode") orelse defaults.testingMode, + .whitelistEnabled = .init(zon.get(bool, "whitelistEnabled") orelse defaults.whitelistEnabled.load(.monotonic)), }; } - pub fn toZon(self: Settings, allocator: NeverFailingAllocator) ZonElement { + pub fn toZon(self: *const Settings, allocator: NeverFailingAllocator) ZonElement { const zon = main.ZonElement.initObject(allocator); zon.put("defaultGamemode", @tagName(self.defaultGamemode)); zon.put("allowCheats", self.allowCheats); zon.put("testingMode", self.testingMode); + zon.put("whitelistEnabled", self.whitelistEnabled.load(.monotonic)); zon.put("seed", self.seed); return zon; @@ -443,7 +446,6 @@ pub const ServerWorld = struct { // MARK: ServerWorld doGameTimeCycle: bool = true, tickSpeed: std.atomic.Value(u32) = .init(12), - whitelistEnabled: std.atomic.Value(bool) = .init(false), settings: Settings = undefined, @@ -651,7 +653,6 @@ pub const ServerWorld = struct { // MARK: ServerWorld self.biomeChecksum = worldData.get(i64, "biomeChecksum") orelse 0; self.name = main.globalAllocator.dupe(u8, worldData.get([]const u8, "name") orelse self.path); self.tickSpeed = .init(worldData.get(u32, "tickSpeed") orelse 12); - self.whitelistEnabled = .init(worldData.get(bool, "whitelistEnabled") orelse false); } pub fn saveWorldConfig(self: *ServerWorld) !void { @@ -667,7 +668,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld worldData.put("name", self.name); worldData.put("lastUsedTime", std.Io.Clock.Timestamp.now(main.io, .real).raw.toMilliseconds()); worldData.put("tickSpeed", self.tickSpeed.load(.monotonic)); - worldData.put("whitelistEnabled", self.whitelistEnabled.load(.monotonic)); + worldData.put("settings", self.settings.toZon(main.stackAllocator)); worldData.put("localPlayer", players.getLocalPlayerIndex()); try files.cubyzDir().writeZon(path, worldData);