Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/network.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.*}});
}
Expand Down
5 changes: 5 additions & 0 deletions src/network/protocols.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a problem with this. There are multiple keys in order to allow switching to a new crypto algorithm when one is broken. So when the server switches this (for testing you can do this in the launchConfig), it will then reject the old player which is still in their list with one of the old keys.

To fix this I think the best solution would be to do this check together with players.lookupIndex in User.identifyFromKeysAndName, so that the old entries are found correctly.

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);
Expand Down
20 changes: 20 additions & 0 deletions src/server/command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 \"<keyType>:<base64>\" for <{s}>, found \"{s}\"", .{name, arg});
Comment thread
IntegratedQuantum marked this conversation as resolved.
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,

Expand Down
1 change: 1 addition & 0 deletions src/server/command/_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
65 changes: 65 additions & 0 deletions src/server/command/whitelist.zig
Original file line number Diff line number Diff line change
@@ -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 <add/block> <keyType>:<base64Key>
\\/whitelist <add/block> @<playerIndex>
Comment thread
IntegratedQuantum marked this conversation as resolved.
\\/whitelist <enable/disable>
;

const Action = enum { add, block };
const Toggle = enum { enable, disable };

pub const Args = union(enum) {
@"/whitelist <action> <key>": struct { action: Action, key: command.KeyString },
@"/whitelist <action> <playerIndex>": struct { action: Action, playerIndex: command.PlayerIndex },
@"/whitelist <enable/disable>": struct { toggle: Toggle },
};

pub fn execute(args: Args, source: Source) void {
switch (args) {
.@"/whitelist <action> <key>" => |params| applyAction(source, params.action, params.key.key),
.@"/whitelist <action> <playerIndex>" => |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 <enable/disable>" => |params| {
main.server.world.?.settings.whitelistEnabled.store(params.toggle == .enable, .monotonic);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change also needs to be stored to disk immediately to avoid losing it when the game doesn't close correctly. (world.saveWorldConfig)

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;
}
}
}
},
}
}
28 changes: 15 additions & 13 deletions src/server/players.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;
}

Expand All @@ -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" {
Expand All @@ -215,25 +216,26 @@ 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();

init("test", 0);

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));
}
6 changes: 5 additions & 1 deletion src/server/world.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = .{};
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading