mas-bandwidth / netcode

Secure client/server connections over UDP
BSD 3-Clause "New" or "Revised" License
2.43k stars 190 forks source link

Question: Connect Token Design #83

Closed RandyGaul closed 5 years ago

RandyGaul commented 5 years ago

Hi there, I was studying the connect token design and have a couple questions. I am just trying to learn about design intentions and use-cases.

  1. timeout seconds - Why is this included in the connect token, instead of living in client/server as a tunable parameter? I was trying to think about use-cases for variable timeout. Is this perhaps somehow useful under DoS circumstances?
  2. Redundant information - Did you ever consider consolidating the connect token public and secret data that have similarities? The redundancy was a bit confusing when reading the standard, and for a while I thought I was having a misunderstanding. A tidbit in the standard describing the intent of redundancy could help make it much simpler to read.
  3. It looks like the only unique information in the encrypted section of the connect token format is the user data and the client id. Are there any use cases (besides testing) for having the public/secret server lists differ from one another (I couldn't think of any)? If not, would a format like this make any sense to you? I consolidated the redundant information, especially the server list, into a public portion of the connection request packet, instead of encrypting the majority of the packet like before. This leads to simplifications in the standard and in the implementation.
// Proposed new design for connect token. The entire token, minus the REST SECTION,
// serves dual purpose: provide the client with necessary information, and also act in
// its entirety as a connection request packet.

// -- BEGIN PUBLIC SECTION --
// --  BEGIN REST SECTION  --
[version info] (13 bytes)       // "NETCODE 1.02" ASCII with null terminator.
[protocol id] (uint64)          // 64 bit value unique to this particular game/application
[client to server key] (32 bytes)
[server to client key] (32 bytes)
// --  END REST SECTION    --
[zero byte]                     // Packet type connection request
[version info] (13 bytes)       // "NETCODE 1.02" ASCII with null terminator.
[protocol id] (uint64)          // 64 bit value unique to this particular game/application
[create timestamp] (uint64)     // 64 bit unix timestamp when this connect token was created
[expire timestamp] (uint64)     // 64 bit unix timestamp when this connect token expires
[timeout seconds] (uint32)      // timeout in seconds. negative values disable timeout (dev only)
[num server addresses] (uint32) // in [1,32]
<for each server address>
{
    [address type] (uint8)      // value of 1 = IPv4 address, 2 = IPv6 address.
    <if IPV4 address>
    {
        // for a given IPv4 address: a.b.c.d:port
        [a] (uint8)
        [b] (uint8)
        [c] (uint8)
        [d] (uint8)
        [port] (uint16)
    }
    <else IPv6 address>
    {
        // for a given IPv6 address: [a:b:c:d:e:f:g:h]:port
        [a] (uint16)
        [b] (uint16)
        [c] (uint16)
        [d] (uint16)
        [e] (uint16)
        [f] (uint16)
        [g] (uint16)
        [h] (uint16)
        [port] (uint16)
    }
}
[connect token nonce] (24 bytes)
<zero pad to 744 bytes>
// --  END PUBLIC SECTION  --
// -- BEGIN SECRET SECTION --
[client id] (uint64)            // globally unique identifier for an authenticated client
[client to server key] (32 bytes)
[server to client key] (32 bytes)
[user data] (256 bytes)         // user defined data specific to this protocol id
// --  END SECRET SECTION  --
[hmac bytes] (16 bytes)

The connection request packet is defined as the connect token minus the REST SECTION. The PUBLIC SECTION of the connection request packet can be used as the additional data in AEAD. This way the connection request packet is entirely protected from tampering, and the client id/userdata/keys are still encrypted. I imagine there is no security risk, since unencrypted information is publicly knowable to authenticated clients anyways.

This design might have some benefits:

Does this consolidation/simplification make any sense? I'm probably missing some obvious problems, and am asking to better understand the netcode design.

P.S. I moved the SECRET SECTION to the end of the packet. This way the SECRET SECTION resides at a known byte-offset, and the server can quickly validate connect tokens, just as before, without the need to parse the server address list.

ghost commented 5 years ago

I had personally assumed that one of the use cases for having redundant server addresses in the secret client data was so that a game server could verify if a client was allowed to connect to that server, without needing to access some sort of central "sessions" database.

RandyGaul commented 5 years ago

@stellarLuminant Right, that makes total sense. I guess a better question would have been "why not make the connect token secret section a lot smaller", since that seems to remove redundancy, simplify things quite a bit, all without sacrificing any security.

Also, I edited my OP a lot -- my apologies! You probably responded to an old version (just fyi in case you had insights to any new questions I wrote).

gafferongames commented 5 years ago

The public token data is unencrypted, because it’s stuff the client needs to connect to the server, eg. The server IP address, private keys, timeout whatever. It’s not encrypted by anything, because the token is sent down from backend to client via a secure channel, typically HTTPS.

The internal part of the token, the private stuff that you think is redundant, is encrypted by a key that the client does not have (by design). The client passes this opaque blob over an unsecure channel (UDP packets) to connect to the server.

The server uses this data to verify that yes, in fact this client is authorized by the backend and is allowed to connect to the server.

That is all. There is no redundancy. You are trying to optimize something you don’t understand. Read the spec again, and understand why the client cannot read, modify or generate the private token data, and why this is necessary for security, then you’ll see why no redundancy exists.

Cheers

RandyGaul commented 5 years ago

Thanks for the response Glenn! I appreciate you helping me to learn about your design.

There’s one thing I’m confused about. Does the typical use-case have the connect token share the same IP address list in both the private and public sections?

If so, is there an error in my proposal? I know you’re busy, and the nature of the question is complicated. You’re right, it’s an attempted optimization. This is typically how I personally learn. I understand it’s a long question though, and I apologize for not being able to shorten it any further. So feel free to ignore if you prefer :)

RandyGaul commented 5 years ago

OK so I just figured out the problem. Not encrypting server IPs lets packet sniffers attempt to steal tokens before they are used, by knowing which server the token can be used to connect with. Which actually leads to a new question... I'll open a different issue.

Edit: Nope sniffing isn't possible: https://github.com/networkprotocol/netcode.io/issues/84. So it still looks like the server list in particular does not need to be secret, so my original proposal still looks ok. All that's needed is for public info to be covered in the HMAC with the Additional Data part of the AEAD.

After thinking about all this some more, I do think the netcode approach is also quite simple, since just encrypting the entire token and duplicating the server list is very straightforward to understand once the design intent is understood. As far as I can see: both approaches should be pretty much equivalent with negligible differences.