diff --git a/src/network.zig b/src/network.zig index 7b6477bf4e..8fc4321a05 100644 --- a/src/network.zig +++ b/src/network.zig @@ -1737,7 +1737,7 @@ 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)}); + 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.*}}); } diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 50a7350942..d8c4db7be0 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.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; + } + var writer: utils.BinaryWriter = .init(main.stackAllocator); defer writer.deinit(); writer.writeEnum(Connection.HandShakeState, .signatureRequest); diff --git a/src/server/command.zig b/src/server/command.zig index 1acdc5472e..d2bcc70a04 100644 --- a/src/server/command.zig +++ b/src/server/command.zig @@ -169,6 +169,26 @@ 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; + }; + 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}; + } +}; + pub const BiomeId = struct { biome: *const main.server.terrain.biomes.Biome, 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..c8e96e83a7 --- /dev/null +++ b/src/server/command/whitelist.zig @@ -0,0 +1,65 @@ +const std = @import("std"); + +const main = @import("main"); +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 @ + \\/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 { + 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); + }, + .@"/whitelist " => |params| { + main.server.world.?.settings.whitelistEnabled.store(params.toggle == .enable, .monotonic); + source.sendMessage("#00ff00Whitelist {s}", .{if (params.toggle == .enable) "enabled" else "disabled"}); + }, + } +} + +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 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 a45051674c..24b8d3d374 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); @@ -185,11 +187,10 @@ pub fn block(key: []const u8) BlockResult { return if (result.wasNew or !wasBlocked) .blocked else .alreadyBlocked; } -pub fn isAllowedToJoin(key: []const u8) bool { - sync.threadContext.assertCorrectContext(.server); +pub fn isAllowedToJoin(key: []const u8, whitelistEnabled: bool) bool { mutex.lock(); defer mutex.unlock(); - const entry = playerDatabase.get(key) orelse return false; + const entry = playerDatabase.get(key) orelse return !whitelistEnabled; return !entry.blocked; } @@ -199,13 +200,13 @@ test "addContainsRemove" { init("test", 0); - try std.testing.expectEqual(false, 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(true, 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(false, isAllowedToJoin("ed25519:abc")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc", true)); } test "addUnblocks" { @@ -215,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(false, isAllowedToJoin("ed25519:xyz", false)); try std.testing.expectEqual(.added, add("ed25519:xyz")); - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz", false)); } -test "knownPlayerAllowedByDefaultButBlockable" { +test "whitelistToggleAffectsUnknownKeysOnly" { main.heap.allocators.createWorldArena(); defer main.heap.allocators.destroyWorldArena(); @@ -228,12 +229,13 @@ 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(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(false, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(false, isAllowedToJoin("ed25519:known", false)); try std.testing.expectEqual(.added, add("ed25519:known")); - try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known")); + try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known", false)); } diff --git a/src/server/world.zig b/src/server/world.zig index 2aba5ac0b0..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; @@ -665,6 +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("settings", self.settings.toZon(main.stackAllocator)); worldData.put("localPlayer", players.getLocalPlayerIndex()); try files.cubyzDir().writeZon(path, worldData);