Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transfer_player function to request client move to new server #14129

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

sfence
Copy link
Contributor

@sfence sfence commented Dec 19, 2023

This is the adoption of #11175.

I have rebased the original @grapereader code. Is compilable, and looks to works well on my Linux system.

This is related to #5813 and #4428 at least.

To do

This PR is a Work in Progress.

  • Add test command or API to devtest game.
  • See feedback and apply potentially required changes

How to test

  1. Create or reuse existing world in devtest game
  2. Host server of it
  3. Run the second Minetest instance, join the hosted server
  4. run command /transfer_player 127.0.0.1 YOUR_PORT_NUMBER
  5. you should be disconnected and reconnected

You can also host the game in the second Minetest instance with the same player_name and password as on the first server and reconnect by the same function from it.

On calling minetest.transfer_player(name, address, port), server
sends a disconnect message of type SERVER_ACCESSDENIED_TRANSFER
with the IP and port packed into customMessage.

Clients that support this new message type will disconnect and
reconnect to the new address:port, using the same username and
password.

Clients that do not support this new message will disconnect and
display the destination server address in the disconnection message
box.
@wsor4035 wsor4035 added @ Server / Client / Env. Feature ✨ PRs that add or enhance a feature Concept approved Approved by a core dev: PRs welcomed! labels Dec 19, 2023
@lhofhansl
Copy link
Contributor

IMHO there needs to be an option to disallow this on a client. Or the user on the client needs to OK through some dialog.

@sfence
Copy link
Contributor Author

sfence commented Dec 20, 2023

IMHO there needs to be an option to disallow this on a client. Or the user on the client needs to OK through some dialog.

Do you mean the global option in minetest.conf or the game-based option in game.conf file?

If it will be disabled by default, upon the game allowing it, can it be a good solution?

@Zughy
Copy link
Contributor

Zughy commented Dec 20, 2023

What happens if I'm registered in the first server but in the second one a different user registered an account with my same nane and different password?

@sfence
Copy link
Contributor Author

sfence commented Dec 20, 2023

What happens if I'm registered in the first server but in the second one a different user registered an account with my same nane and different password?

You will be disconnected from the first server and connecting to the second server returns an auth error, so the effect will be similar to disconnecting from the first server.

Copy link
Contributor

@appgurueu appgurueu left a comment

Choose a reason for hiding this comment

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

Convenient feature, however, given that Minetest uses unencrypted UDP packets, I'm mildly concerned about "redirect" attacks. For example, hypothetically, couldn't a malicious server first harvest player IPs, then just constantly spam "transfer player" requests to these IPs to "steal" the users?

I've also tried to review the C++ code. Hopefully one of the core devs can review my review.

@@ -61,7 +61,17 @@ function ui.update()
local formspec = {}

-- handle errors
if gamedata ~= nil and gamedata.reconnect_requested then
if gamedata ~= nil and gamedata.reconnect_requested and #gamedata.transfer_address > 0 then
Copy link
Contributor

Choose a reason for hiding this comment

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

#gamedata.transfer_address > 0 would be slightly more readable as gamedata.transfer_address ~= "" (also: why not use nil to indicate an absent transfer address?)

@@ -6589,6 +6589,9 @@ Bans
optional reason, this will not prefix with 'Kicked: ' like kick_player.
If no reason is given, it will default to 'Disconnected.'
* Returns boolean indicating success (false if player nonexistent)
* `minetest.transfer_player(name, address, port)`: Disconnect a player and
request the client re-connect at a new address and port. The connection
to the new server will occur using the same player name and password.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: "will occur using the same player name" - what about the name parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is expected to be called by some mod on the server side. So that mod has to identify a player.

As there is name parameter for minetest.disconnect_player and minetest.kick_player, there is a name parameter for minetest.transfer_player.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I framed that poorly. I understand the API. I just find the wording confusing, since (for obvious reasons) we don't pass a password parameter, yet it says "using the same player name and password". Suggestion: Something along the lines of "[...] will occur using name and the client-remembered password" maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my understanding: A password is remembered in some form of hash or something similar. This value is remembered and reused with the player's name to send the AUTH packet to the new server.

Copy link
Contributor

@appgurueu appgurueu Dec 30, 2023

Choose a reason for hiding this comment

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

I think this PR will actually remember the password in plaintext, in client memory? (Which isn't optimal for security, btw.)

SRP still only sends hashes over the network though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have started working on it. I will create a PR to fix it as soon as possible.
This PR will be forced to wait for that PR to be merged, and I rebase this after it and fix the problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have started working on it. I will create a PR to fix it as soon as possible. This PR will be forced to wait for that PR to be merged, and I rebase this after it and fix the problem.

Won't this PR have to remember the password though to do the SRP auth with the server without requiring the user to re-enter their password? Or is there a way to just remember a hash or something similar? (Which would be ideal.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If there is a way to remember SRP auth without a password, it is an ideal situation. It looks promising.
Otherwise, the client will be forced to reenter the password.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #14196.
Maybe, something more can be done in the main menu Lua scripts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -327,6 +327,9 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef

bool reconnectRequested() const { return m_access_denied_reconnect; }

std::string getReconnectAddress() const { return m_access_denied_reconnect_address; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not return const std::string &s here?

@@ -228,11 +231,14 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
core::rect<s32>(0, 0, 10000, 10000));

bool game_has_run = launch_game(error_message, reconnect_requested,
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel at this point the three reconnect parameters might deserve their own struct? It seems to me this would make the code somewhat less verbose.

@@ -724,6 +724,8 @@ class Game {
const GameStartData &game_params,
std::string &error_message,
bool *reconnect,
std::string &reconnect_address,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not make these const?

@@ -4525,7 +4531,9 @@ void the_game(bool *kill,
const GameStartData &start_data,
std::string &error_message,
ChatBackend &chat_backend,
bool *reconnect_requested) // Used for local game
bool *reconnect_requested,
std::string &reconnect_address,
Copy link
Contributor

Choose a reason for hiding this comment

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

const?

{
NO_MAP_LOCK_REQUIRED;

const char *playerName = lua_tolstring(L, 1, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't readParam<std::string> work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably will work. I have rebased it, not rewritten it.

std::string addressPack;
*pkt >> addressPack;

m_access_denied_reason = accessDeniedStrings[denyCode];
Copy link
Contributor

Choose a reason for hiding this comment

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

Being network-facing code, I think this should be more careful. Shouldn't there be an index check here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that this is fine because of the condition above. denyCode has to be equal to SERVER_ACCESSDENIED_TRANSFER here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, that's true. (Though I don't see why we do the array lookup then - might as well hardcode the string here...)

m_access_denied_reason = accessDeniedStrings[denyCode];
m_access_denied_reconnect = true;

size_t splitAt = addressPack.find(';');
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest adding a check to deal with the case of ; not being found.

auto addressPack = address + ";" + port;

for (RemotePlayer *player : m_players) {
if (name.compare(player->getName()) == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there some kind of "get player by name" function you could use?

I also think that this should ideally throw an error if no player with the given name is found (rather than failing silently).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be probably good to return false as l_disconnect_player in l_server.cpp does.

Copy link
Contributor

Choose a reason for hiding this comment

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

That depends on whether you would consider this a frequent, recoverable exception (that often does not need to be handled) or an "unrecoverable" "panic" / failure, IMO. I think passing a wrong player name is clearly a logic issue and hence of the latter kind, so should trigger an error. Modders can just check whether a player is online if they want the "lax" behavior (or they could use pcall).

@sfence
Copy link
Contributor Author

sfence commented Dec 30, 2023

@appgurueu
The problem of "redirecting" attacks and similar should be fixed with issue #10206.

@sfence
Copy link
Contributor Author

sfence commented Dec 31, 2023

This PR should be blocked by PR #14196 until it is merged or refused.

@red-001
Copy link
Contributor

red-001 commented Aug 30, 2024

I feel this really needs #14226 or at least a way to pass (signed) data to the other server.
Minetest already includes a SHA256 implementation, could fairly easily implement HMAC-SHA256 (should be ~30-50 LOC) and use that with a shared secret the server owners can agree upon. And at that point could add a flag to use an empty password for the other server.

I'm just thinking about possible use-cases for this, I don't really see many at all for transferring a player to a server unknown to the first. It in the worst case even come off as hostile from the point of view of the owner that's suddenly getting unexpected players that didn't join their server in the usual way. On the flip side, for servers where the owners know each other or the same person runs both servers, it might be very useful to pass meta-data or even skip login altogether since the first server already verified the user.

@sfence
Copy link
Contributor Author

sfence commented Aug 30, 2024

@red-001
The idea is that server-to-server communication can be done via the https lua API. Because of it, #14226 has been closed.

Transferring to another server can be used in worlds that have dimensions... like space and planets, normal world and nether... etc...
It can be good for performance requirements because every dimension can run on different HW.

@red-001
Copy link
Contributor

red-001 commented Aug 31, 2024

Transferring to another server can be used in worlds that have dimensions... like space and planets, normal world and nether... etc...
It can be good for performance requirements because every dimension can run on different HW.

Those all seem like examples were you would want to be able to pass some metadata with the player and skip login. Generic server to server communications are probably not related, I shouldn't have mentioned that PR.

@sfence
Copy link
Contributor Author

sfence commented Aug 31, 2024

Those all seem like examples were you would want to be able to pass some metadata with the player and skip login. Generic server to server communications are probably not related, I shouldn't have mentioned that PR.

It is not related until you want to sync inventories for example. (fly to moon, mine metals, return back to planet and build some technic staff for example)

@Zughy Zughy added the Waiting (on dependency) Waiting on another PR or external circumstances (not for rebases/changes requested) label Aug 31, 2024
@appgurueu
Copy link
Contributor

I feel this really needs #14226 or at least a way to pass (signed) data to the other server.

That could be nice to have, but I don't think this is needed for this PR to be useful.

There are plenty of other options for server owners willing to exchange data between their servers. Via trusted mods and luasocket or similar, they can implement whatever they please. The HTTP API is also an option.

@red-001
Copy link
Contributor

red-001 commented Sep 3, 2024

Per discussion on IRC, I don't think there is that much bad that an attacker can do by redirecting the player and they would need to be able to either intercept packets, or guess the player peer id, IP address and port number.

Assuming the client won't attempt to register a new account on the server it's redirected to, it doesn't let an attacker than can read packets do anything new. Otherwise this could have allowed them to capture the verifier.
If the attacker can only blindly inject packets, this could open the door to upgrading that to an active packet interception attack, by redirecting the player to an attacker controlled server and forwarding packets to the "real" server.

Could reduce the risk of that by implementing #14988. Should be more difficult to correctly guess the peer_id, IP address, port number and sequence number.

That could be nice to have, but I don't think this is needed for this PR to be useful.

There are plenty of other options for server owners willing to exchange data between their servers. Via trusted mods and luasocket or similar, they can implement whatever they please. The HTTP API is also an option.

Yeah I agree, I just meant there should be some way to pass associated data with the client. Keeps everything nice and in-sync and would allow for more creative access control setups.

Could for instance have a separate realm that someone can join from the main server if they have the correct item (like the Nether in one of those other Voxel games) or a mini-games server you can be redirected to from the main one.
Can't do that as easily if you can't give the player a token they will give the server they are connecting to. If it's any easier could make the data unprotected and leave it to mods to implement JWT or whatever they want to use. I'm really just thinking of some small amount of metadata that can serve as a password replacement and some small amount of configuration data.

Actually could remove the concern around redirects from blind injection attacks mentioned above if a "token" was mandatory instead of a password.

@MisterE123
Copy link
Contributor

Could we get this for 5.12?

@sfence
Copy link
Contributor Author

sfence commented Feb 4, 2025

@MisterE123 Because of the change request here and the state of #14196, I do not expect this to move forward soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Concept approved Approved by a core dev: PRs welcomed! Feature ✨ PRs that add or enhance a feature @ Server / Client / Env. Waiting (on dependency) Waiting on another PR or external circumstances (not for rebases/changes requested)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants