Skip to content

Make Synchronization step optional for non-UDP transport layers #95

@caspark

Description

@caspark

What

The synchronization process currently happens at the protocol layer, and I think it would be better if it happened at the socket layer (if at all), because (I believe) it isn't actually needed for other network transports like matchbox, steam networking, renet, etc.

Why

Moving the sync process to the swappable transport layer would allow users of other transports like matchbox to skip the synchronization process, which would be beneficial in a few ways:

  • Synchronization causes a variable delay in starting a match, which can't be skipped.
  • Synchronization is an extra step to figure out in solving built-in-to-ggrs support for hot join (or reconnecting disconnected players) - which is what prompted me to raise this issue.
  • As far as I can tell, synchronization doesn't do anything that is strictly necessary for natively-multiplexed-and-authenticated transports - more on this below.
  • Lastly, there is currently no way for synchronization to time out - it just hangs indefinitely if e.g. a remote client doesn't respond. (Obviously this can be fixed separately, but by moving synchronization out of the protocol layer this issue will only affect those who use the default UDP transport.)

The Synchronization Process

Currently GGRS uses the same "synchronization" process as GGPO; to summarize:

  • Before starting the game simulation, P2P and spectator sessions will send a SyncRequest containing a (randomly generated) number to each (anticipated to be) connected client.
  • When a client receives a SyncRequest, it sends a SyncReply back with the same number.
  • When a client receives a SyncReply, it checks whether this reply's number matches that in the last request it sent, and drops the message if not.
    • If it does match, and this is the 4th or less SyncReply, the process is repeated
    • Otherwise, the 5th SyncReply's header's "magic number" (a different a random number) is stored as the final remote_magic in the protocol's state; this is used to drop future messages whose headers' "magic" does not match the stored remote_magic.

Purpose of Synchronizing

As far as I can tell based on GGPO and GGRS source code and (a lot of) guessing, here's why synchronizing is in place:

  1. Handshaking: making sure that the other side actually has a ggrs client listening on that UDP port.
    • Needed for UDP: not strictly needed - if the remote doesn't actually connect, they will instead be disconnected as part of the usual disconnection of non-responsive peers.
    • Needed by other transports: no - matchbox, renet, iroh, steam networking, quinn all handle authentication of a peer as part of their own transport duties.
  2. Basic stateful-firewall/NAT traversal: by sending an outbound UDP packet to IP:PORT, many stateful firewalls/NAT gateways will automatically allow/forward an incoming response from IP:PORT too - so both clients synchronizing will bypass some NATs.
    • Needed for UDP: yes, or at least, it's necessary but insufficient for a production game: a robust implementation of NAT traversal would require quite a bit more features - STUN, TURN or similar relaying, etc.
    • Needed by other transports: no - because other transports do their own handshaking to connect, they have to do their own NAT traversal too (the synchronization's NAT hole punching would be "too late" for them).
  3. Basic authentication: the protocol can drop packets that aren't from any of known peers (i.e. when packets' headers' magics don't match the stored remote magic)
    • Needed for UDP: yes for a production game - since source IP and port can be spoofed, attackers could send messages with different inputs, which would cause desyncs.
    • Needed by other transports: no - as far as I'm aware, each of matchbox, renet, iroh and steam networking do their own (more robust) authentication.

So as I reason right now, other transports don't benefit from the synchronization process, and it isn't free, so I think it shouldn't be part of P2P and spectator sessions?

Options

That leaves us with a few approaches:

  1. Remove synchronization entirely: if ggrs is meant to be focused on the tricky logic of rollbacks and ggrs::UdpNonBlockingSocket is just a bare minimum demo implementation of the NonBlockingSocket (i.e. it is expected to be swapped out entirely for any "serious" game), then I think removing synchronization entirely is a reasonable approach.
    • I've done this in my personal ggrs fork (caspark@f7b8fdd) and it has the expected effect: everything seems to work as before with both UDP and Matchbox sockets (on both good and terrible connections), except for that 1) there isn't a pause at the start to wait for synchronization and 2) a client that fails to join the game will be disconnected (which is a feature in my opinion)
  2. Move the synchronizing logic from the sessions and protocol down to the socket layer (into ggrs::UdpNonBlockingSocket). Doesn't regress any functionality, but will require a bit of refactoring to allow ggrs-users to still observe synchronization progress & completion (UDP socket will need to emit events, so would have to expose a way to get at the socket or expect users to Rc/Arc-wrap the socket).
  3. Add a skip_synchronization(bool) (default false) to the GGRS session builder, which starts a session off in synchronized state - tell using users to turn it on if using a transport that satisfies criteria X/Y/Z. Easiest to implement by far but feels like a hack to me.

So if you're happy with approach 1 then I can PR that pretty quickly - or if you prefer #2 then I could have a shot at that.

Or if you don't want to explore this, you can just close the issue :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions