diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index cff6a5f2..517f9d13 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -34,9 +34,11 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick: that with where it had the player at that tick, not now, and moves the player by the difference, keeping what they did since. The host takes a client's player where the client says within a tolerance of its own, - and no further from where it last took them than a player moves in the - client's ticks since (so the tolerance is not gained again each tick); - a teleporter, which moves the host's own copy too, starts afresh. + and no further from an anchor (one it took, a newer once a second) than + a player moves in the client's ticks since, those no more than its own + since and a little jitter (so the tolerance and the jitter are not gained + again each tick); a teleporter, which moves the host's own copy too, + starts afresh. - **Shooter's hits.** A client reports what its own players hit; the host checks the report (the player's, a weapon they carry, the target where the host had it when the shooter saw it, no faster than weapons fire) and @@ -124,10 +126,11 @@ a pregame keep-alive every five seconds from the host only that client is told; - twice a second and with every kill, the players' statistics that changed (once a second a few more, round them all, as the message is - unreliable); when it changes (looked at five times a second) and once a - second, the game type's state (scores, the flags, the balls and their - carriers, the king's hill, the players' speeds) and whether the game is - over. A machine that has loaded is sent all of both at once. It counts + unreliable); when it changes (looked at five times a second, sent at + most every other look unless the game ended; reliably, so never + again unchanged), the game type's state (scores, the flags, the balls + and their carriers, the king's hill, the players' speeds) and whether + the game is over. A machine that has loaded is sent all of both at once. It counts the game type's events (captures, grabs and returns of the flags, laps, the balls reset), and a client announces those it has not had, as the host does its own; a client runs the rest of the game type's @@ -173,11 +176,12 @@ a pregame keep-alive every five seconds from the host drawn fading over a few ticks (`render_interpolation.c`) instead of a jump. A client drives its own player's vehicle and sends where it is, which the host takes within a tolerance, as it does its own player's - unit, and no further from where it last took it than the vehicle moves - in the client's ticks since (its speed, the host's copy's or twice the - tag's top speed, whichever is more, no more than 3 world units a tick, - and a tenth more a tick, with the blend distance; a teleporter falls - back to the tolerance). The host sends the client its own vehicle + unit, and no further from an anchor (one it took, a newer once a + second) than the vehicle moves in the client's ticks since, those no + more than its own since and a little jitter (its speed, the host's + copy's or twice the tag's top speed, whichever is more, no more than 3 + world units a tick, and a tenth more a tick, with the blend distance; a + teleporter falls back to the tolerance). The host sends the client its own vehicle every third tick, with the client's tick it took the vehicle at (`_distributed_object_predicted_bit` and a 16-bit time in the object's state); the client compares that with where it had the vehicle at that @@ -271,7 +275,7 @@ Unity's Netcode for Entities, lightyear, netfox and the Ares source): round all of them, four a tick; the players' statistics when they change (with every kill, twice a second), with sixteen more players' once a second round them all; the game type's state when it - changes, and once a second. + changes, at most two and a half times a second. - **The players each client needs, when it needs them.** The host sends a client every player's unit and input every tick when they are within 25 world units of the client's own players, every second tick within 60, diff --git a/port/linux/README.md b/port/linux/README.md index 6ee644e7..fbbc515b 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -355,6 +355,10 @@ Only machines with the invite can find the game: (`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer with a key that only it and the player can calculate. Its public key must agree with the identifier in the link. +- Then the player shows in the same way that it has the private key of its + public key. Only then does the host make a session for the player. Thus + other machines with the invite cannot make sessions in the name of a + player (such a session would keep the player out). - Each two machines get the keys of their packets from their key pairs and a random number from each. The keys do not go through the brokers. Thus other machines with the invite cannot read or change the packets. diff --git a/port/linux/game/network_damage.c b/port/linux/game/network_damage.c index b4c36532..cf8594c5 100644 --- a/port/linux/game/network_damage.c +++ b/port/linux/game/network_damage.c @@ -452,7 +452,8 @@ void network_damage_aftermath( short body_part, short node_index, short region_index, - short material_index) + short material_index, + long victim_player_index) { struct distributed_damage_event *event; struct unit_datum *unit; @@ -464,8 +465,9 @@ void network_damage_aftermath( unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit); if (!unit) return; - /* (the last events kept for killing blows) */ - kill = TEST_FLAG(being_damaged_flags, _object_being_damaged_body_depleted_bit) && unit->unit.player_index != NONE; + /* (the last events kept for killing blows: of a player's unit, whose + player the blow's aftermath has already taken from it, unit_died) */ + kill = TEST_FLAG(being_damaged_flags, _object_being_damaged_body_depleted_bit) && victim_player_index != NONE; if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS)) return; event = &damage_events[damage_event_count++]; @@ -490,7 +492,7 @@ void network_damage_aftermath( event->kind = _damage_event_kill; /* (no killer when the game noted none: a death it did not score) */ - if (!distributed_get_death((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index), + if (!distributed_get_death((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(victim_player_index), &event->player_index, &friendly_fire, &killed_by_vehicle)) { event->player_index = NO_PLAYER; @@ -1234,8 +1236,30 @@ static void distributed_note_event_destinations( } /* (a player's body, living or dead) */ unit = (struct unit_datum *)object_try_and_get_and_verify_type(event->object_index, _object_mask_unit); - if (unit && unit->unit.player_index != NONE) - player_indices[(*count)++] = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index); + if (unit) + { + long unit_player_index = unit->unit.player_index; + + /* (a body's player is no longer its unit's, but has it as their + dead one, player_died) */ + if (unit_player_index == NONE) + { + struct data_iterator iterator; + struct player_datum *player; + + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + if (player->dead_unit_index == event->object_index) + { + unit_player_index = iterator.datum_index; + break; + } + } + } + if (unit_player_index != NONE) + player_indices[(*count)++] = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_player_index); + } if (tick->time != game_time_get()) continue; for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++) diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 46014dd1..dda4864e 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -90,7 +90,10 @@ enum STATISTICS_REFRESH_PLAYERS = 16, /* the game type's state looked at, and sent unchanged */ GAME_STATE_INTERVAL_TICKS = 6, - GAME_STATE_REFRESH_TICKS = TICKS_PER_SECOND, + /* ... when it changes (reliably: no refresh), no more often than this + (the king's and the ball's scores change every tick), but the game's end + at once */ + GAME_STATE_MINIMUM_TICKS = 2 * GAME_STATE_INTERVAL_TICKS, MAXIMUM_GAME_STATE_SIZE = 0xF00, MAXIMUM_STATISTICS_PER_MESSAGE = 64, MAXIMUM_PICKUPS_PER_TICK = 64, @@ -113,9 +116,13 @@ enum /* the host: the ticks after it took a client's player's position that it still tells the client which of its ticks it has them at */ PREDICTION_ECHO_TICKS = 3, - /* ... and the client's ticks it takes as run between two of its own - beyond the host's (its messages delayed, then bunched) */ + /* ... and the client's ticks it takes as run since the anchor beyond + the host's (its messages delayed, then bunched) */ PREDICTION_JITTER_TICKS = 6, + /* ... how long it measures predictions from one it took, the anchor, + before it takes a newer as the anchor (the jitter and the blend granted + once a second, not once a tick) */ + PREDICTION_ANCHOR_TICKS = TICKS_PER_SECOND, }; /* struct distributed_unit_state flags */ @@ -383,9 +390,10 @@ static struct long taken_time; long taken_host_time; } distributed_predictions[MAXIMUM_TRACKED_PLAYERS]; -/* the host: where it last took each client's player to be (the next may -be no further from it than the player moves in the client's ticks since), -at which of the client's ticks and its own, and where that left its unit */ +/* the host: the anchor, where it took each client's player to be (one +taken, a newer once a second: the next may be no further from it than the +player moves in the client's ticks since), at which of the client's ticks +and its own; and when it last took one, and where that left its unit */ static struct { boolean valid; @@ -393,7 +401,8 @@ static struct long time; long host_time; real_point3d position; - real_point3d host_position; + long taken_host_time; + real_point3d taken_host_position; } distributed_accepted[MAXIMUM_TRACKED_PLAYERS]; /* a client: where each of its own players' units was at its last ticks, and how long the host takes to have them (ticks) */ @@ -450,8 +459,10 @@ static long distributed_player_machines[MAXIMUM_TRACKED_PLAYERS]; and once a second a few players' whatever they are, round them all) */ static unsigned long distributed_sent_statistics[MAXIMUM_TRACKED_PLAYERS]; static short distributed_statistics_cursor; -/* the host: the game type's state as last sent, and when */ +/* the host: the game type's state as last sent (its checksum and its +leading postgame state), and when */ static unsigned long distributed_game_state_checksum; +static long distributed_game_state_postgame; static long distributed_game_state_time; /* the latest tick of each kind of unreliable message had from each sender @@ -1529,11 +1540,12 @@ static void distributed_handle_predictions( little off closed by half, more put there. Each accepted moves the host's unit, and the next is measured from there, so a client could take its player HOST_ACCEPT_TOLERANCE further each tick: a prediction is also no -further from the last one taken than the player moves in the client's -ticks since (MAXIMUM_PREDICTED_SPEED), those no more than the host's since -and a little jitter. A teleporter moves the host's own unit too, further -than that since the last one taken: it is measured from where the host has -it only (as it is after a new life, or a ride). */ +further from the anchor (one taken, a newer once a second) than the player +moves in the client's ticks since (MAXIMUM_PREDICTED_SPEED), those no more +than the host's since and a little jitter. A teleporter moves the host's +own unit too, further than it moves since the last taken: it is measured +from where the host has it only, a new anchor (as it is after a new life, +or a ride). */ static void distributed_apply_predictions( void) { @@ -1568,16 +1580,17 @@ static void distributed_apply_predictions( continue; if (distributed_accepted[player_index].valid) { - long host_ticks = now - distributed_accepted[player_index].host_time; long ticks = MIN(distributed_predictions[player_index].time - distributed_accepted[player_index].time, - host_ticks + PREDICTION_JITTER_TICKS); - real reach = MAXIMUM_PREDICTED_SPEED * (real)host_ticks + HOST_BLEND_DISTANCE; + now - distributed_accepted[player_index].host_time + PREDICTION_JITTER_TICKS); + real reach = MAXIMUM_PREDICTED_SPEED * (real)(now - distributed_accepted[player_index].taken_host_time) + + HOST_BLEND_DISTANCE; - dx = object->object.position.x - distributed_accepted[player_index].host_position.x; - dy = object->object.position.y - distributed_accepted[player_index].host_position.y; - dz = object->object.position.z - distributed_accepted[player_index].host_position.z; - /* (the host's own unit moved further than a unit moves: taken - from where it is) */ + dx = object->object.position.x - distributed_accepted[player_index].taken_host_position.x; + dy = object->object.position.y - distributed_accepted[player_index].taken_host_position.y; + dz = object->object.position.z - distributed_accepted[player_index].taken_host_position.z; + /* (the host's own unit moved further than a unit moves since + the last taken, which only the host moves it by: taken from + where it is, a new anchor) */ if (!(dx * dx + dy * dy + dz * dz <= reach * reach)) distributed_accepted[player_index].valid = FALSE; else @@ -1596,12 +1609,18 @@ static void distributed_apply_predictions( { distributed_predictions[player_index].taken_time = distributed_predictions[player_index].time; distributed_predictions[player_index].taken_host_time = now; - distributed_accepted[player_index].valid = TRUE; - distributed_accepted[player_index].unit_index = unit_index; - distributed_accepted[player_index].time = distributed_predictions[player_index].time; - distributed_accepted[player_index].host_time = now; - distributed_accepted[player_index].position = state->position; - distributed_accepted[player_index].host_position = object->object.position; + /* (a new anchor: the first, one a second on, or after a jump) */ + if (!distributed_accepted[player_index].valid || + now - distributed_accepted[player_index].host_time >= PREDICTION_ANCHOR_TICKS) + { + distributed_accepted[player_index].valid = TRUE; + distributed_accepted[player_index].unit_index = unit_index; + distributed_accepted[player_index].time = distributed_predictions[player_index].time; + distributed_accepted[player_index].host_time = now; + distributed_accepted[player_index].position = state->position; + } + distributed_accepted[player_index].taken_host_time = now; + distributed_accepted[player_index].taken_host_position = object->object.position; } } } @@ -2527,8 +2546,8 @@ static void distributed_send_all_statistics( /* ---------- the game type's state */ /* the game type's state (larger than a datagram: reliably) to a machine -that has loaded, or (NONE) to every client when it changed or it has not -been sent for a second */ +that has loaded, or (NONE) to every client when it changed (at most every +other look, unless the game ended) */ static void distributed_send_game_state( long machine_index) { @@ -2549,11 +2568,16 @@ static void distributed_send_game_state( return; } checksum = distributed_checksum(message.data, size); - if (checksum == distributed_game_state_checksum && distributed_game_state_time != NONE && - game_time_get() - distributed_game_state_time < GAME_STATE_REFRESH_TICKS) + if (distributed_game_state_time != NONE && + (checksum == distributed_game_state_checksum || + (game_time_get() - distributed_game_state_time < GAME_STATE_MINIMUM_TICKS && + (size < (long)sizeof(long) || + csmemcmp(message.data, &distributed_game_state_postgame, sizeof(long)) == 0)))) { return; } + if (size >= (long)sizeof(long)) + csmemcpy(&distributed_game_state_postgame, message.data, sizeof(long)); distributed_game_state_checksum = checksum; distributed_game_state_time = game_time_get(); distributed_send(&message, _distributed_message_game_state, 0, (word)(sizeof(message.header) + size), @@ -2584,6 +2608,7 @@ void network_distributed_new_game( csmemset(distributed_sent_statistics, 0, sizeof(distributed_sent_statistics)); distributed_statistics_cursor = 0; distributed_game_state_checksum = 0; + distributed_game_state_postgame = 0; distributed_game_state_time = NONE; distributed_host_update_number = NONE; for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++) diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index f21fd2bc..e56cd88f 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -100,9 +100,9 @@ enum up to the most */ CLIENT_READY_INTERVAL_TICKS = TICKS_PER_SECOND, CLIENT_READY_MAXIMUM_INTERVAL_TICKS = 4 * TICKS_PER_SECOND, - /* (a machine that does not say whether it asks again: network_objects_ - client_ready) the host tells it all its objects at most once in this - long (asked again sooner, they are on their way) */ + /* the host tells a machine all its objects again at most once in this + long, asked again (a client that failed to make one: asked again sooner, + they are on their way; a flood of asking is not answered in full) */ HOST_OBJECTS_RESEND_TICKS = 10 * TICKS_PER_SECOND, /* a client that failed to make one of the host's objects asks for them all again this long after, twice as long each time it fails again up to @@ -126,15 +126,17 @@ enum /* a client: where the vehicles its own players drive were, the last ticks (a power of two, more than the longest round trip) */ OWN_VEHICLE_POSITION_TICKS = 64, - /* the host: the ticks after it took a client's vehicle that it still - tells the client which of its ticks it has it at */ - VEHICLE_PREDICTION_ECHO_TICKS = 3, - /* ... and the client's ticks it takes as run between two it took beyond - the host's (its messages delayed, then bunched) */ + /* the host: the client's ticks it takes as run since a client's vehicle's + anchor beyond its own (its messages delayed, then bunched) */ VEHICLE_PREDICTION_JITTER_TICKS = 6, - /* ... and how long it measures the next from the last it took (longer, - from where it has the vehicle only: a ride begun again) */ - VEHICLE_PREDICTION_REFERENCE_TICKS = TICKS_PER_SECOND, + /* ... how long it measures predictions from one it took, the anchor, + before it takes a newer as the anchor (the jitter and the blend granted + once a second, not once a tick) */ + VEHICLE_PREDICTION_ANCHOR_TICKS = TICKS_PER_SECOND, + /* ... and how long, with none taken, it keeps the anchor, and tells the + client which of its ticks it has the vehicle at (longer: from where it + has the vehicle only, a ride begun again) */ + VEHICLE_PREDICTION_REFERENCE_TICKS = 2 * TICKS_PER_SECOND, /* a client: its own player's ammunition and grenades are the host's once the host has had this long past a round trip to see what it did with them (the host's inventories go every INVENTORY_INTERVAL_TICKS) */ @@ -312,20 +314,22 @@ static struct long player_indices[MAXIMUM_LOCAL_PLAYERS]; } objects_host_machines[HALO_PORT_MAXIMUM_NETWORK_MACHINES]; /* ... each client's player's latest vehicle prediction, taken at the next -tick; whether it took the latest it had; the vehicle it last took (NONE: -none), at which of the client's ticks and its own, where the client had it -and where that left the host's */ +tick; the vehicle it last took (NONE: none), at which of the client's ticks +and its own, and where that left the host's copy; and the anchor +predictions are measured from: one it took, at which of the client's ticks +and its own, and where the client had it */ static struct { boolean valid; - boolean echo; long machine_index; struct distributed_object_state state; long accepted_vehicle_index; word accepted_time; long accepted_host_time; - real_point3d accepted_position; real_point3d accepted_host_position; + word anchor_time; + long anchor_host_time; + real_point3d anchor_position; } objects_host_vehicle_predictions[MAXIMUM_TRACKED_PLAYERS]; /* a client: the host's objects it has, by absolute index */ static long objects_client_has[MAXIMUM_TRACKED_OBJECTS]; @@ -820,7 +824,8 @@ void network_objects_client_asked( return; player_list = machine_get_player_list(machine_index); /* (another machine in its place has other players) */ - if (again || objects_host_machines[machine_index].time == NONE || + if ((again && game_time_get() - objects_host_machines[machine_index].time >= HOST_OBJECTS_RESEND_TICKS) || + objects_host_machines[machine_index].time == NONE || csmemcmp(objects_host_machines[machine_index].player_indices, player_list, sizeof(objects_host_machines[machine_index].player_indices)) != 0) { @@ -1050,11 +1055,12 @@ static void distributed_host_send_states( { short player_index = objects_host_viewers.machines[machine_number].vehicle_player_indices[own_vehicle]; + /* (the client's clock against the host's, which a gap or a + prediction refused since leaves as it was) */ if (player_index >= 0 && player_index < MAXIMUM_TRACKED_PLAYERS && - objects_host_vehicle_predictions[player_index].echo && objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index && game_time_get() - objects_host_vehicle_predictions[player_index].accepted_host_time < - VEHICLE_PREDICTION_ECHO_TICKS) + VEHICLE_PREDICTION_REFERENCE_TICKS) { SET_FLAG(state->flags, _distributed_object_predicted_bit, TRUE); state->time = (short)(word)(objects_host_vehicle_predictions[player_index].accepted_time + @@ -1307,13 +1313,14 @@ void network_objects_handle_vehicle_prediction( still drives it: a little off closed by half, more put there. Each taken moves the host's copy, and the next is measured from there, so a client could take its vehicle HOST_VEHICLE_ACCEPT_TOLERANCE further each tick: a -prediction is also no further from the last one taken than the vehicle goes -in the client's ticks since (those no more than the host's since and a -little jitter), and no faster: as fast as twice its tag's top speed, or as -the host's copy goes (falling, thrown), whichever is faster, with a margin. -A teleporter moves the host's copy too, further than that since the last one -taken: it is measured from where the host has it only. Which of the -client's ticks the host has it at is noted, to tell the client. */ +prediction is also no further from the anchor (one taken, a newer once a +second) than the vehicle goes in the client's ticks since (after it, and no +more than the host's since and a little jitter), and no faster: as fast as +twice its tag's top speed, or as the host's copy goes (falling, thrown), +whichever is faster, with a margin. A teleporter moves the host's copy too, +further than it goes since the last taken: it is measured from where the +host has it only, a new anchor. Which of the client's ticks the host has it at is noted, to tell +the client. */ void network_objects_apply_vehicle_predictions( void) { @@ -1328,11 +1335,11 @@ void network_objects_apply_vehicle_predictions( real_vector3d forward, up, velocity, angular_velocity; real dx, dy, dz; real speed; + boolean anchored; if (!objects_host_vehicle_predictions[player_index].valid) continue; objects_host_vehicle_predictions[player_index].valid = FALSE; - objects_host_vehicle_predictions[player_index].echo = FALSE; vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(state->object_index, _object_mask_vehicle); if (!vehicle || vehicle->unit.driver_object_index == NONE || vehicle->object.parent_object_index != NONE) continue; @@ -1363,27 +1370,36 @@ void network_objects_apply_vehicle_predictions( speed = top_speed; speed = MIN(speed, MAXIMUM_PREDICTED_VEHICLE_SPEED); } - if (objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index && - now - objects_host_vehicle_predictions[player_index].accepted_host_time < VEHICLE_PREDICTION_REFERENCE_TICKS) + anchored = objects_host_vehicle_predictions[player_index].accepted_vehicle_index == state->object_index && + now - objects_host_vehicle_predictions[player_index].anchor_host_time < VEHICLE_PREDICTION_REFERENCE_TICKS; + if (anchored) { long host_ticks = now - objects_host_vehicle_predictions[player_index].accepted_host_time; - long ticks = MIN((long)(short)(word)((word)state->time - objects_host_vehicle_predictions[player_index].accepted_time), - host_ticks + VEHICLE_PREDICTION_JITTER_TICKS); real reach = (speed + PREDICTED_VEHICLE_SPEED_MARGIN) * (real)host_ticks + HOST_VEHICLE_BLEND_DISTANCE; - real_point3d const *accepted = &objects_host_vehicle_predictions[player_index].accepted_host_position; + real_point3d const *from = &objects_host_vehicle_predictions[player_index].accepted_host_position; - dx = vehicle->object.position.x - accepted->x; - dy = vehicle->object.position.y - accepted->y; - dz = vehicle->object.position.z - accepted->z; - /* (the host's copy moved further than it goes: taken from where - it is) */ - if (dx * dx + dy * dy + dz * dz <= reach * reach) + dx = vehicle->object.position.x - from->x; + dy = vehicle->object.position.y - from->y; + dz = vehicle->object.position.z - from->z; + /* (the host's copy moved further than it goes since the last + taken, which only the host moves it by: taken from where it is, + a new anchor) */ + if (!(dx * dx + dy * dy + dz * dz <= reach * reach)) { + anchored = FALSE; + } + else + { + /* (a client's clock that jumped counts no more than the + host's ticks since and the jitter) */ + long ticks = MIN((long)(short)(word)((word)state->time - objects_host_vehicle_predictions[player_index].anchor_time), + now - objects_host_vehicle_predictions[player_index].anchor_host_time + VEHICLE_PREDICTION_JITTER_TICKS); + reach = (speed + PREDICTED_VEHICLE_SPEED_MARGIN) * (real)ticks + HOST_VEHICLE_BLEND_DISTANCE; - accepted = &objects_host_vehicle_predictions[player_index].accepted_position; - dx = state->position.x - accepted->x; - dy = state->position.y - accepted->y; - dz = state->position.z - accepted->z; + from = &objects_host_vehicle_predictions[player_index].anchor_position; + dx = state->position.x - from->x; + dy = state->position.y - from->y; + dz = state->position.z - from->z; if (ticks <= 0 || !(dx * dx + dy * dy + dz * dz <= reach * reach)) continue; } @@ -1396,12 +1412,18 @@ void network_objects_apply_vehicle_predictions( continue; network_objects_reconcile(state->object_index, &state->position, &forward, &up, &velocity, &angular_velocity, HOST_VEHICLE_BLEND_DISTANCE); - objects_host_vehicle_predictions[player_index].echo = TRUE; objects_host_vehicle_predictions[player_index].accepted_vehicle_index = state->object_index; objects_host_vehicle_predictions[player_index].accepted_time = (word)state->time; objects_host_vehicle_predictions[player_index].accepted_host_time = now; - objects_host_vehicle_predictions[player_index].accepted_position = state->position; objects_host_vehicle_predictions[player_index].accepted_host_position = vehicle->object.position; + /* (a new anchor: the first, one a second on, or after a jump) */ + if (!anchored || + now - objects_host_vehicle_predictions[player_index].anchor_host_time >= VEHICLE_PREDICTION_ANCHOR_TICKS) + { + objects_host_vehicle_predictions[player_index].anchor_time = (word)state->time; + objects_host_vehicle_predictions[player_index].anchor_host_time = now; + objects_host_vehicle_predictions[player_index].anchor_position = state->position; + } } } diff --git a/port/linux/src/p2p.c b/port/linux/src/p2p.c index 202f2e12..3c834780 100644 --- a/port/linux/src/p2p.c +++ b/port/linux/src/p2p.c @@ -13,9 +13,10 @@ link games as if they were on one LAN, without a server of this project's. - Signalling (p2p_signal.c) goes through public MQTT brokers, on topics that are hashes of the token, with messages sealed with a key derived from it (p2p_crypto.c). A joiner offers its public key and the addresses - it can be reached at; the host answers with its own. The secret of their - session comes from the two keys (X25519) and a nonce of each, and never - travels. + it can be reached at; the host answers with its own, and makes the + session once the joiner, answered, proves that it holds its key. The + secret of their session comes from the two keys (X25519) and a nonce of + each, and never travels. - The tunnel is one UDP socket. Each machine learns its public address from public STUN servers, and both then send to each other's addresses until packets get through (hole punching). There is no relay: two machines whose @@ -1213,7 +1214,8 @@ static int find_closed(int stream, unsigned long *address, unsigned short *port) return 0; } -/* a stand-in that opens has a port no closed one stands for now */ +/* a stand-in, or a socket of the game's, that has the port now: no closed +stand-in stands for it */ static void forget_closed(int stream, unsigned short local_port) { int index; @@ -1570,6 +1572,17 @@ void p2p_socket_port(int socket, int stream, int listening, unsigned short port) /* the game listens for connections while it hosts */ if (stream && listening) p2p.hosting_socket = socket; + /* (and no stand-in that closed has the port now) */ + forget_closed(stream != 0, port); + pthread_mutex_unlock(&p2p_lock); +} + +void p2p_port_taken(int stream, unsigned short port) +{ + if (!p2p.running || !port) + return; + pthread_mutex_lock(&p2p_lock); + forget_closed(stream != 0, port); pthread_mutex_unlock(&p2p_lock); } diff --git a/port/linux/src/p2p.h b/port/linux/src/p2p.h index 73eae937..31d97714 100644 --- a/port/linux/src/p2p.h +++ b/port/linux/src/p2p.h @@ -60,6 +60,9 @@ int p2p_broadcast_datagram(unsigned short source_port, unsigned short port, cons (stream and listening: it is hosting). Peers reach only these ports (a stream's only while it listens), and datagram ports it sent them from */ void p2p_socket_port(int socket, int stream, int listening, unsigned short port); +/* a socket that peers are not to reach (bound to 127.0.0.1 alone) has this +local port: traffic from it is not a peer's whose stand-in had the port */ +void p2p_port_taken(int stream, unsigned short port); /* the game closes a socket */ void p2p_socket_closed(int socket); diff --git a/port/linux/src/p2p_signal.c b/port/linux/src/p2p_signal.c index 1879f011..f7bf50c4 100644 --- a/port/linux/src/p2p_signal.c +++ b/port/linux/src/p2p_signal.c @@ -10,27 +10,40 @@ Everything that passes through them is sealed with a key derived from the invite's token, and goes to topics that are hashes of it, so the brokers (and anyone watching them) learn nothing and can join nothing: -- the host listens on hceu/2/, where a joiner +- the host listens on hceu/3/, where a joiner sends JOIN: its public key (its identifier is the key's hash), a nonce, and its addresses; -- the joiner listens on hceu/2/, where the +- the joiner listens on hceu/3/, where the host answers ACCEPT: its public key, the joiner's nonce, one of its own, its addresses, and a tag that only the two of them can make (from their - keys). + keys); +- the joiner then repeats its JOIN with the host's nonce and a tag of its + own, made the same way, which shows that it holds the key it gave. Only + then does the host make a session. Their tunnel's keys come from their X25519 shared secret and the two nonces, and never travel: another holder of the invite reads the messages but cannot work them out, and cannot answer as the host (whose key must hash to the identifier in the invite). It can send a JOIN in another -machine's name, which gets it nothing: the tunnel takes that machine's key, -and a session with a machine is not replaced while it lives (p2p.c). +machine's name, but cannot prove it: the host answers it (to that machine) +and makes no session of it. A session no one could complete would keep that +machine out while it lived (a session with a machine is not replaced while +it lives, p2p.c), and a JOIN every so often would keep it out for good. + +The host's nonce is a hash of the request with a key of the host's and the +time (it changes every HOST_NONCE_PERIOD), so the host need not remember +what it answered: anyone with the invite can ask in a machine's name as +often as it likes, pushing out whatever the host remembered. What it does +keep of requests not proven yet (the work of their keys) only saves work. A joiner repeats its JOIN until the tunnel reaches the host. The host makes -one session of a JOIN (its public key and nonce) at most: anyone watching -the brokers could send it again after the session ended, and have the host -reach for the joiner's old addresses in its name, keeping it out. So a -joiner asks with a new nonce when its session with the host ends before the -tunnel reached it, or when the host has not answered it in a while. +one session of a request (a public key and nonce) at most: anyone watching +the brokers could send the proven JOIN again after the session ended, and +have the host reach for the joiner's old addresses in its name, keeping it +out (with the same keys again). It remembers the request at least while its +host nonce lasts. So a joiner asks with a new nonce when its session with +the host ends before the tunnel reached it, or when the host has not +answered it in a while. */ #include "platform.h" @@ -45,16 +58,22 @@ tunnel reached it, or when the host has not answered it in a while. enum { MAXIMUM_BROKERS = 4, - /* the joiners a host remembers, so a repeated request gets the same - session: as many as it takes */ + /* the joiners a host made sessions for, so a repeated request gets the + same session: as many as it takes */ MAXIMUM_JOINERS = P2P_MAXIMUM_PEERS + 1, + /* the requests not proven yet whose keys' work a host keeps; only to + save work (a flood of requests pushes them out) */ + MAXIMUM_ASKERS = 16, /* the requests a session was made from, which make none again (a - host takes at most a few new players a minute: this is hours of them) */ + host takes at most a few new players a minute: this is hours of them; + one is not forgotten while its host nonce lasts: USED_REQUEST_TIME) */ MAXIMUM_USED_REQUESTS = 1024, TOPIC_SIZE = 7 + 32 + 1, NONCE_SIZE = 8, - /* an ACCEPT's tag: the first half of an HMAC-SHA256 */ + /* an ACCEPT's or a proven JOIN's tag: the first half of an HMAC-SHA256 */ TAG_SIZE = 16, + /* a proven JOIN's end: the host's nonce, and the tag */ + PROOF_SIZE = NONCE_SIZE + TAG_SIZE, BUFFER_SIZE = 4096, MAXIMUM_MESSAGE_SIZE = 256, @@ -67,9 +86,12 @@ enum JOIN_INTERVAL = 2000, ANSWER_INTERVAL = 1000, /* a joiner the host has not answered in this long asks anew, with a - new nonce (the host may have made a session from its request whose - answer was lost, and makes no other from it) */ + new nonce (a fresh start: the host makes nothing of a request it did + not answer, as it makes a session only of a proven one) */ UNANSWERED_TIME = 20000, + /* a host nonce is taken in the period it is made in and the next */ + HOST_NONCE_PERIOD = 30000, + USED_REQUEST_TIME = 2 * HOST_NONCE_PERIOD, }; enum @@ -84,7 +106,8 @@ enum { _message_join = 'J', _message_accept = 'A', - MESSAGE_VERSION = 2, + /* 3: a JOIN proves its key before the host makes a session */ + MESSAGE_VERSION = 3, }; struct broker @@ -109,6 +132,8 @@ struct broker int output_size; }; +/* a request: of a session made (joiners), or not proven yet (askers, which +have no host nonce or secret) */ struct joiner { unsigned char identifier[P2P_IDENTIFIER_SIZE]; @@ -123,6 +148,13 @@ struct joiner int used; }; +struct used_request +{ + /* the joiner's identifier and nonce */ + unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE]; + unsigned long time; +}; + static struct { int started; @@ -137,9 +169,14 @@ static struct char host_topic[TOPIC_SIZE]; struct joiner joiners[MAXIMUM_JOINERS]; int next_joiner; - /* the joiner's identifier and nonce of each request a session was made - from (kept while hosting stops and starts: an invite lasts the run) */ - unsigned char used_requests[MAXIMUM_USED_REQUESTS][P2P_IDENTIFIER_SIZE + NONCE_SIZE]; + struct joiner askers[MAXIMUM_ASKERS]; + int next_asker; + /* the key of the host's nonces (host_nonce_for), for the run */ + int has_nonce_key; + unsigned char nonce_key[P2P_SHA256_SIZE]; + /* each request a session was made from (kept while hosting stops and + starts: an invite lasts the run) */ + struct used_request used_requests[MAXIMUM_USED_REQUESTS]; int used_request_count; int used_request_next; @@ -155,9 +192,11 @@ static struct char join_host_topic[TOPIC_SIZE]; char join_topic[TOPIC_SIZE]; unsigned long join_sent_time; - /* when join_nonce was made, and whether the host answered it */ + /* when join_nonce was made, and whether the host answered it (with + join_host_nonce, which its proof carries) */ unsigned long join_nonce_time; int join_answered; + unsigned char join_host_nonce[NONCE_SIZE]; } signalling; static int elapsed(unsigned long since, unsigned long time) @@ -196,7 +235,7 @@ static void make_topic(const unsigned char *token, const char *label, const unsi derive(token, label, identifier, digest); p2p_hex(digest, 16, text); - snprintf(topic, TOPIC_SIZE, "hceu/2/%s", text); + snprintf(topic, TOPIC_SIZE, "hceu/3/%s", text); } /* ---------- MQTT */ @@ -463,18 +502,63 @@ static void session_secret(const unsigned char *base, const unsigned char *nonce p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, sizeof(data), secret); } -/* an ACCEPT's tag, over what precedes it */ -static void accept_tag(const unsigned char *base, const unsigned char *message, int size, unsigned char *tag) +/* an ACCEPT's tag (label "accept") or a proven JOIN's ("join"), over what +precedes it */ +static void message_tag(const unsigned char *base, const char *label, const unsigned char *message, int size, + unsigned char *tag) { unsigned char data[6 + MAXIMUM_MESSAGE_SIZE]; unsigned char digest[P2P_SHA256_SIZE]; + int label_size = (int)strlen(label); - memcpy(data, "accept", 6); - memcpy(data + 6, message, (size_t)size); - p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, 6 + size, digest); + memcpy(data, label, (size_t)label_size); + memcpy(data + label_size, message, (size_t)size); + p2p_hmac_sha256(base, P2P_SHA256_SIZE, data, label_size + size, digest); memcpy(tag, digest, TAG_SIZE); } +/* whether a message's tag (its last TAG_SIZE bytes) is right */ +static int tag_right(const unsigned char *base, const char *label, const unsigned char *message, int size) +{ + unsigned char tag[TAG_SIZE]; + + message_tag(base, label, message, size - TAG_SIZE, tag); + return p2p_equal(tag, message + size - TAG_SIZE, TAG_SIZE); +} + +/* the host's nonce for a request (a joiner's public key and nonce) in a +period of HOST_NONCE_PERIOD */ +static void host_nonce_for(const unsigned char *public_key, const unsigned char *nonce, unsigned long period, + unsigned char *host_nonce) +{ + unsigned char data[4 + P2P_KEY_SIZE + NONCE_SIZE]; + unsigned char digest[P2P_SHA256_SIZE]; + int index; + + for (index = 0; index < 4; index++) + data[index] = (unsigned char)(period >> (index * 8)); + memcpy(data + 4, public_key, P2P_KEY_SIZE); + memcpy(data + 4 + P2P_KEY_SIZE, nonce, NONCE_SIZE); + p2p_hmac_sha256(signalling.nonce_key, P2P_SHA256_SIZE, data, sizeof(data), digest); + memcpy(host_nonce, digest, NONCE_SIZE); +} + +/* whether the host answered a request with this nonce lately: in this +period, or the last */ +static int host_nonce_current(const unsigned char *public_key, const unsigned char *nonce, + const unsigned char *host_nonce) +{ + unsigned long period = p2p_now() / HOST_NONCE_PERIOD; + unsigned char expected[NONCE_SIZE]; + + host_nonce_for(public_key, nonce, period, expected); + if (p2p_equal(expected, host_nonce, NONCE_SIZE)) + return 1; + /* (the last before the clock wraps, before the first) */ + host_nonce_for(public_key, nonce, period ? period - 1 : 0xFFFFFFFFUL / HOST_NONCE_PERIOD, expected); + return p2p_equal(expected, host_nonce, NONCE_SIZE); +} + /* ---------- the messages */ static void send_join(void) @@ -490,107 +574,208 @@ static void send_join(void) memcpy(message + size, signalling.join_nonce, NONCE_SIZE); size += NONCE_SIZE; size += put_candidates(message + size); + /* answered: the proof that this machine holds its key, of which the host + makes the session */ + if (signalling.join_answered) + { + memcpy(message + size, signalling.join_host_nonce, NONCE_SIZE); + size += NONCE_SIZE; + message_tag(signalling.join_base, "join", message, size, message + size); + size += TAG_SIZE; + } size = p2p_seal(signalling.join_key, message, size, sealed); publish_everywhere(signalling.join_host_topic, sealed, size); signalling.join_sent_time = p2p_now(); } -/* the host: a joiner asked */ +static struct joiner *find_joiner(struct joiner *list, int count, const unsigned char *public_key, + const unsigned char *nonce) +{ + int index; + + for (index = 0; index < count; index++) + { + if (list[index].used && !memcmp(list[index].public_key, public_key, P2P_KEY_SIZE) && + !memcmp(list[index].nonce, nonce, NONCE_SIZE)) + { + return &list[index]; + } + } + return NULL; +} + +/* pair_base with a joiner: kept from a request of its, else worked out */ +static int joiner_base(const unsigned char *public_key, unsigned char *base) +{ + int index; + + for (index = 0; index < MAXIMUM_JOINERS + MAXIMUM_ASKERS; index++) + { + struct joiner const *joiner = index < MAXIMUM_JOINERS ? &signalling.joiners[index] : + &signalling.askers[index - MAXIMUM_JOINERS]; + + if (joiner->used && !memcmp(joiner->public_key, public_key, P2P_KEY_SIZE)) + { + memcpy(base, joiner->base, P2P_SHA256_SIZE); + return 1; + } + } + return pair_base(public_key, p2p_public_key(), public_key, base); +} + +static int request_used(const unsigned char *request) +{ + int index; + + for (index = 0; index < signalling.used_request_count; index++) + { + if (!memcmp(signalling.used_requests[index].request, request, P2P_IDENTIFIER_SIZE + NONCE_SIZE)) + return 1; + } + return 0; +} + +/* the host's answer to a request */ +static void send_accept(const unsigned char *identifier, const unsigned char *nonce, const unsigned char *host_nonce, + const unsigned char *base) +{ + unsigned char answer[MAXIMUM_MESSAGE_SIZE]; + unsigned char sealed[MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD]; + char topic[TOPIC_SIZE]; + int size = 0; + + answer[size++] = _message_accept; + answer[size++] = MESSAGE_VERSION; + memcpy(answer + size, p2p_public_key(), P2P_KEY_SIZE); + size += P2P_KEY_SIZE; + memcpy(answer + size, nonce, NONCE_SIZE); + size += NONCE_SIZE; + memcpy(answer + size, host_nonce, NONCE_SIZE); + size += NONCE_SIZE; + size += put_candidates(answer + size); + message_tag(base, "accept", answer, size, answer + size); + size += TAG_SIZE; + size = p2p_seal(signalling.host_key, answer, size, sealed); + make_topic(signalling.host_token, "joiner", identifier, topic); + publish_everywhere(topic, sealed, size); +} + +/* the host: a joiner asked; with a proof (the host's nonce, and a tag) once +the host answered it */ static void join_received(const unsigned char *message, int size) { struct p2p_candidate candidates[P2P_MAXIMUM_CANDIDATES]; - unsigned char answer[MAXIMUM_MESSAGE_SIZE]; - unsigned char sealed[MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD]; unsigned char identifier[P2P_IDENTIFIER_SIZE]; + unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE]; + unsigned char host_nonce[NONCE_SIZE]; + unsigned char base[P2P_SHA256_SIZE]; + unsigned char secret[P2P_SHA256_SIZE]; const unsigned char *public_key = message + 2; const unsigned char *nonce = public_key + P2P_KEY_SIZE; int fixed = 2 + P2P_KEY_SIZE + NONCE_SIZE; - struct joiner *joiner = NULL; - char topic[TOPIC_SIZE]; - int answered = 0; + struct joiner *joiner; + struct used_request *used; + int proven; int count; - int index; - int answer_size = 0; if (size < fixed + 1) return; count = get_candidates(message + fixed, size - fixed, candidates); if (count < 0) return; + proven = size - fixed - 1 - count * 6; + if (proven != 0 && proven != PROOF_SIZE) + return; p2p_identifier_for(public_key, identifier); - for (index = 0; index < MAXIMUM_JOINERS; index++) + joiner = find_joiner(signalling.joiners, MAXIMUM_JOINERS, public_key, nonce); + if (joiner) { - if (signalling.joiners[index].used && - !memcmp(signalling.joiners[index].identifier, identifier, P2P_IDENTIFIER_SIZE)) + /* a request a session was made from, again (through another broker, + or repeated until the tunnel reaches the host): that session's + answer, while it lasts; never another. The addresses only from a + proof (anyone can send the rest) */ + if (proven && (memcmp(message + size - PROOF_SIZE, joiner->host_nonce, NONCE_SIZE) || + !tag_right(joiner->base, "join", message, size))) { - joiner = &signalling.joiners[index]; - break; - } - } - if (joiner && !memcmp(joiner->public_key, public_key, P2P_KEY_SIZE) && !memcmp(joiner->nonce, nonce, NONCE_SIZE)) - { - /* the same request again, through another broker or repeated: the - same session, while it lasts; never another (anyone watching the - brokers can send it again once the session ends, and a session in - the joiner's name would keep the joiner out while it lasts: a joiner - asks again with a new nonce) */ - if (!elapsed(joiner->answered_time, ANSWER_INTERVAL) || - !p2p_peer_reoffered(identifier, joiner->secret, candidates, count)) return; - answered = 1; - } - if (!answered) - { - /* a new session (p2p.c turns it away while another with that machine - lives), from a request no session was made from */ - unsigned char base[P2P_SHA256_SIZE], host_nonce[NONCE_SIZE], secret[P2P_SHA256_SIZE]; - unsigned char request[P2P_IDENTIFIER_SIZE + NONCE_SIZE]; - - memcpy(request, identifier, P2P_IDENTIFIER_SIZE); - memcpy(request + P2P_IDENTIFIER_SIZE, nonce, NONCE_SIZE); - for (index = 0; index < signalling.used_request_count; index++) - { - if (!memcmp(signalling.used_requests[index], request, sizeof(request))) - return; } + if (!elapsed(joiner->answered_time, ANSWER_INTERVAL) || + !p2p_peer_reoffered(identifier, joiner->secret, candidates, proven ? count : 0)) + { + return; + } + joiner->answered_time = p2p_now(); + send_accept(identifier, joiner->nonce, joiner->host_nonce, joiner->base); + return; + } + memcpy(request, identifier, P2P_IDENTIFIER_SIZE); + memcpy(request + P2P_IDENTIFIER_SIZE, nonce, NONCE_SIZE); + if (request_used(request)) + return; + if (!proven) + { + /* an answer (to the machine whose key it is, which alone can prove + the request), and nothing else: the host keeps nothing of it that it + needs */ + struct joiner *asker = find_joiner(signalling.askers, MAXIMUM_ASKERS, public_key, nonce); + + if (asker && !elapsed(asker->answered_time, ANSWER_INTERVAL)) + return; /* (checked before the work of the keys, which anyone with the invite can ask for as often as they like) */ - if (p2p_peer_turned_away(identifier, 0) || !pair_base(public_key, p2p_public_key(), public_key, base)) + if (p2p_peer_turned_away(identifier, 0)) return; - posix_random_bytes(host_nonce, NONCE_SIZE); - session_secret(base, nonce, host_nonce, secret); - if (!p2p_peer_offered(identifier, secret, candidates, count, 0)) - return; - memcpy(signalling.used_requests[signalling.used_request_next], request, sizeof(request)); - signalling.used_request_next = (signalling.used_request_next + 1) % MAXIMUM_USED_REQUESTS; - if (signalling.used_request_count < MAXIMUM_USED_REQUESTS) - signalling.used_request_count++; - if (!joiner) - joiner = &signalling.joiners[signalling.next_joiner++ % MAXIMUM_JOINERS]; - memcpy(joiner->identifier, identifier, P2P_IDENTIFIER_SIZE); - memcpy(joiner->public_key, public_key, P2P_KEY_SIZE); - memcpy(joiner->nonce, nonce, NONCE_SIZE); - memcpy(joiner->host_nonce, host_nonce, NONCE_SIZE); - memcpy(joiner->base, base, P2P_SHA256_SIZE); - memcpy(joiner->secret, secret, P2P_SHA256_SIZE); - joiner->used = 1; + if (!asker) + { + if (!joiner_base(public_key, base)) + return; + asker = &signalling.askers[signalling.next_asker]; + signalling.next_asker = (signalling.next_asker + 1) % MAXIMUM_ASKERS; + memset(asker, 0, sizeof(*asker)); + memcpy(asker->identifier, identifier, P2P_IDENTIFIER_SIZE); + memcpy(asker->public_key, public_key, P2P_KEY_SIZE); + memcpy(asker->nonce, nonce, NONCE_SIZE); + memcpy(asker->base, base, P2P_SHA256_SIZE); + asker->used = 1; + } + asker->answered_time = p2p_now(); + host_nonce_for(public_key, nonce, p2p_now() / HOST_NONCE_PERIOD, host_nonce); + send_accept(identifier, nonce, host_nonce, asker->base); + return; } + /* proven: with a nonce the host answered the request with lately, and a + tag only the key's holder can make. A new session (p2p.c turns it away + while another with that machine lives) */ + memcpy(host_nonce, message + size - PROOF_SIZE, NONCE_SIZE); + if (!host_nonce_current(public_key, nonce, host_nonce) || p2p_peer_turned_away(identifier, 0) || + !joiner_base(public_key, base) || !tag_right(base, "join", message, size)) + { + return; + } + /* (a request is remembered while its proof lasts, at least: a copy of it + would make the session again, with the same keys) */ + used = &signalling.used_requests[signalling.used_request_next]; + if (signalling.used_request_count == MAXIMUM_USED_REQUESTS && !elapsed(used->time, USED_REQUEST_TIME)) + return; + session_secret(base, nonce, host_nonce, secret); + if (!p2p_peer_offered(identifier, secret, candidates, count, 0)) + return; + memcpy(used->request, request, sizeof(request)); + used->time = p2p_now(); + signalling.used_request_next = (signalling.used_request_next + 1) % MAXIMUM_USED_REQUESTS; + if (signalling.used_request_count < MAXIMUM_USED_REQUESTS) + signalling.used_request_count++; + joiner = &signalling.joiners[signalling.next_joiner]; + signalling.next_joiner = (signalling.next_joiner + 1) % MAXIMUM_JOINERS; + memcpy(joiner->identifier, identifier, P2P_IDENTIFIER_SIZE); + memcpy(joiner->public_key, public_key, P2P_KEY_SIZE); + memcpy(joiner->nonce, nonce, NONCE_SIZE); + memcpy(joiner->host_nonce, host_nonce, NONCE_SIZE); + memcpy(joiner->base, base, P2P_SHA256_SIZE); + memcpy(joiner->secret, secret, P2P_SHA256_SIZE); joiner->answered_time = p2p_now(); - - answer[answer_size++] = _message_accept; - answer[answer_size++] = MESSAGE_VERSION; - memcpy(answer + answer_size, p2p_public_key(), P2P_KEY_SIZE); - answer_size += P2P_KEY_SIZE; - memcpy(answer + answer_size, joiner->nonce, NONCE_SIZE); - answer_size += NONCE_SIZE; - memcpy(answer + answer_size, joiner->host_nonce, NONCE_SIZE); - answer_size += NONCE_SIZE; - answer_size += put_candidates(answer + answer_size); - accept_tag(joiner->base, answer, answer_size, answer + answer_size); - answer_size += TAG_SIZE; - answer_size = p2p_seal(signalling.host_key, answer, answer_size, sealed); - make_topic(signalling.host_token, "joiner", identifier, topic); - publish_everywhere(topic, sealed, answer_size); + joiner->used = 1; + send_accept(identifier, nonce, host_nonce, base); } /* the joiner: the host answered */ @@ -598,7 +783,6 @@ static void accept_received(const unsigned char *message, int size) { struct p2p_candidate candidates[P2P_MAXIMUM_CANDIDATES]; unsigned char identifier[P2P_IDENTIFIER_SIZE]; - unsigned char tag[TAG_SIZE]; unsigned char secret[P2P_SHA256_SIZE]; const unsigned char *host_public = message + 2; const unsigned char *nonce = host_public + P2P_KEY_SIZE; @@ -608,6 +792,13 @@ static void accept_received(const unsigned char *message, int size) if (size < fixed + 1 + TAG_SIZE || memcmp(nonce, signalling.join_nonce, NONCE_SIZE)) return; + /* the first answer to the request holds: the proof carries its host + nonce (the host's changes every HOST_NONCE_PERIOD) */ + if (signalling.join_answered && (memcmp(host_public, signalling.join_host_public, P2P_KEY_SIZE) || + memcmp(host_nonce, signalling.join_host_nonce, NONCE_SIZE))) + { + return; + } /* the invite's host: its key hashes to the identifier in the invite */ p2p_identifier_for(host_public, identifier); if (memcmp(identifier, signalling.join_host, P2P_IDENTIFIER_SIZE)) @@ -620,15 +811,18 @@ static void accept_received(const unsigned char *message, int size) signalling.join_has_base = 1; } /* and the answer is its */ - accept_tag(signalling.join_base, message, size - TAG_SIZE, tag); - if (!p2p_equal(tag, message + size - TAG_SIZE, TAG_SIZE)) + if (!tag_right(signalling.join_base, "accept", message, size)) return; count = get_candidates(message + fixed, size - TAG_SIZE - fixed, candidates); if (count < 0) return; - signalling.join_answered = 1; session_secret(signalling.join_base, nonce, host_nonce, secret); - p2p_peer_offered(signalling.join_host, secret, candidates, count, 1); + if (!p2p_peer_offered(signalling.join_host, secret, candidates, count, 1) || signalling.join_answered) + return; + memcpy(signalling.join_host_nonce, host_nonce, NONCE_SIZE); + signalling.join_answered = 1; + /* the proof, at once */ + send_join(); } static void publish_received(const char *topic, const unsigned char *payload, int size) @@ -721,10 +915,11 @@ static void broker_parse(struct broker *broker) publish_received(topic, body + offset, remaining - offset); } } - memmove(broker->input, broker->input + total, (size_t)(broker->input_size - total)); - broker->input_size -= total; + /* (what it sent in answer may have closed it, emptying input) */ if (broker->socket < 0) return; + memmove(broker->input, broker->input + total, (size_t)(broker->input_size - total)); + broker->input_size -= total; } } @@ -909,6 +1104,11 @@ static void sync_all_topics(void) void p2p_signal_host(const unsigned char *token) { + if (!signalling.has_nonce_key) + { + posix_random_bytes(signalling.nonce_key, P2P_SHA256_SIZE); + signalling.has_nonce_key = 1; + } memcpy(signalling.host_token, token, P2P_TOKEN_SIZE); derive(token, "seal", NULL, signalling.host_key); make_topic(token, "host", p2p_identifier(), signalling.host_topic); @@ -920,6 +1120,7 @@ void p2p_signal_stop_hosting(void) { signalling.hosting = 0; memset(signalling.joiners, 0, sizeof(signalling.joiners)); + memset(signalling.askers, 0, sizeof(signalling.askers)); sync_all_topics(); } diff --git a/port/linux/src/posix_net.c b/port/linux/src/posix_net.c index cd49128b..9528ced6 100644 --- a/port/linux/src/posix_net.c +++ b/port/linux/src/posix_net.c @@ -333,9 +333,9 @@ int posix_socket_select(int *read, int *read_count, int *write, int *write_count /* poll, which takes any descriptor (select none from FD_SETSIZE on, which a process allowed more files has), with select's readiness: read for data, the end or an error, write for room or an error, error for - urgent data */ + urgent data or (as Winsock's) a connect that failed */ static const short events[3] = { POLLIN, POLLOUT, POLLPRI }; - static const short ready[3] = { POLLIN | POLLHUP | POLLERR, POLLOUT | POLLERR, POLLPRI }; + static const short ready[3] = { POLLIN | POLLHUP | POLLERR, POLLOUT | POLLERR, POLLPRI | POLLERR }; /* (larger sets, as internet play's thread waits on, in a buffer each thread keeps: not one allocation each time) */ static __thread struct pollfd *buffer; diff --git a/port/linux/src/xnet.c b/port/linux/src/xnet.c index 62a5d0ad..f3236641 100644 --- a/port/linux/src/xnet.c +++ b/port/linux/src/xnet.c @@ -240,8 +240,8 @@ static int peer_outgoing_address(int stream, int connecting, const struct sockad return 1; } -/* tells internet play the local port the game's socket has (bound, or -listening) */ +/* tells internet play the local port the game's socket has (bound, given +one by a connection or a send, or listening) */ static void note_socket_port(SOCKET socket, int listening) { struct sockaddr_in bound; @@ -249,15 +249,18 @@ static void note_socket_port(SOCKET socket, int listening) int type = SOCK_DGRAM; int type_length = sizeof(type); - /* (not one bound to this machine alone: the telnet console's, which no - peer is to reach, and which is not hosting) */ if (posix_socket_getsockname((int)socket, &bound, &length) < 0 || bound.sin_family != AF_INET || - bound.sin_addr.s_addr == loopback_address()) + !bound.sin_port) { return; } posix_socket_getsockopt((int)socket, SOL_SOCKET, SO_TYPE, &type, &type_length); - p2p_socket_port((int)socket, type == SOCK_STREAM, listening, bound.sin_port); + /* (one bound to this machine alone, as the telnet console's, is not for + peers to reach, and is not hosting; its port is still no stand-in's) */ + if (bound.sin_addr.s_addr == loopback_address()) + p2p_port_taken(type == SOCK_STREAM, bound.sin_port); + else + p2p_socket_port((int)socket, type == SOCK_STREAM, listening, bound.sin_port); } /* traffic from an internet play peer's stand-in comes from the peer, and @@ -406,6 +409,8 @@ int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int ad unsigned long override; int type = SOCK_STREAM; int type_length = sizeof(type); + int result; + int error; /* an internet play peer's TCP or UDP port */ if (address && address->sa_family == AF_INET && @@ -437,7 +442,18 @@ int WSAAPI halo_ws_connect(SOCKET socket, const struct sockaddr *address, int ad posix_socket_bind((int)socket, &bound, sizeof(bound)); } } - return winsock_result(posix_socket_connect((int)socket, address, address_length)); + result = posix_socket_connect((int)socket, address, address_length); + error = result < 0 ? posix_socket_last_error() : 0; + /* the port the system gave it (a connection under way has one too, and + a datagram socket's that failed keeps its: a stream socket's that failed + has none) */ + note_socket_port(socket, 0); + if (result < 0) + { + WSASetLastError(error); + return SOCKET_ERROR; + } + return result; } int WSAAPI halo_ws_listen(SOCKET socket, int backlog) @@ -474,6 +490,10 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla { /* the rewritten destination: it must outlive the send */ struct sockaddr_in target; + /* 0: not bound yet, which the first send binds it */ + unsigned short source_port = socket_port(socket); + int result; + int send_error; if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) && ((const struct sockaddr_in *)address)->sin_addr.s_addr == INADDR_BROADCAST) @@ -481,11 +501,8 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla unsigned long targets[MAXIMUM_BROADCAST_TARGETS]; unsigned short ports[P2P_BROADCAST_PEERS]; int target_count = broadcast_targets(targets, MAXIMUM_BROADCAST_TARGETS); - unsigned short source_port; int peer_count; - int send_error; int index; - int result; /* one datagram per target; the broadcast counts as sent if any is */ memcpy(&target, address, sizeof(target)); @@ -511,7 +528,12 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla /* and to every internet play peer (after the send above, which binds the socket if it was not): onto the tunnel at once, else through the stand-ins */ - source_port = socket_port(socket); + if (!source_port) + { + source_port = socket_port(socket); + if (source_port) + note_socket_port(socket, 0); + } if (source_port) { if (p2p_broadcast_datagram(source_port, ((const struct sockaddr_in *)address)->sin_port, buffer, @@ -551,8 +573,6 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla if (address && address->sa_family == AF_INET && address_length >= (int)sizeof(struct sockaddr_in) && (halo_ws_ntohl(((const struct sockaddr_in *)address)->sin_addr.s_addr) & 0xFFC00000) == 0x64400000) { - unsigned short source_port = socket_port(socket); - switch (source_port ? p2p_send_datagram(source_port, ((const struct sockaddr_in *)address)->sin_addr.s_addr, ((const struct sockaddr_in *)address)->sin_port, buffer, length) : 0) { @@ -571,7 +591,17 @@ int WSAAPI halo_ws_sendto(SOCKET socket, const char *buffer, int length, int fla address = outgoing_address(address, address_length, &target); break; } - return winsock_result(posix_socket_sendto((int)socket, buffer, length, flags, address, address_length)); + result = posix_socket_sendto((int)socket, buffer, length, flags, address, address_length); + send_error = result < 0 ? posix_socket_last_error() : 0; + /* the port the send gave it (one that failed binds it too) */ + if (!source_port) + note_socket_port(socket, 0); + if (result < 0) + { + WSASetLastError(send_error); + return SOCKET_ERROR; + } + return result; } /* debug.network_latency and debug.network_loss: what this machine receives diff --git a/source/bungie_net/network/transport_endpoint_winsock.c b/source/bungie_net/network/transport_endpoint_winsock.c index 3db81a95..4a5e7f6c 100644 --- a/source/bungie_net/network/transport_endpoint_winsock.c +++ b/source/bungie_net/network/transport_endpoint_winsock.c @@ -1228,30 +1228,35 @@ short connect_endpoint( do { fd_set writeable; + fd_set failed; writeable.fd_array[0] = ep->socket; writeable.fd_count = 1; + failed.fd_array[0] = ep->socket; + failed.fd_count = 1; - if (select(1, NULL, &writeable, NULL, &timeval) == 1) + /* port: a connect that failed is in the error set (not + writeable, and its would-block left as it was) */ + if (select(1, NULL, &writeable, &failed, &timeval) > 0) { - error = 0; + error = failed.fd_count ? WSAECONNREFUSED : 0; } else { error = WSAGetLastError(); } - if (system_milliseconds() > timeout) + /* port: a select that ends with the connection still being + made leaves the connect's would-block: waited on, as in + progress is, until the timeout (a connection made after a + SYN sent again takes a second or three) */ + if ((error == WSAEINPROGRESS || error == WSAEWOULDBLOCK) && + (long)(system_milliseconds() - timeout) > 0) { - error = WSAEINPROGRESS; - closesocket(ep->socket); - /* port: closed here, not again when the endpoint is (its - number may be another socket's by then) */ - ep->socket = INVALID_SOCKET; break; } } - while (error == WSAEINPROGRESS); + while (error == WSAEINPROGRESS || error == WSAEWOULDBLOCK); } } @@ -1259,6 +1264,12 @@ short connect_endpoint( { winsock_error_to_string(error); result = _transport_error_connect_failed; + /* port: none of the attempt kept (its socket half made and not + blocking, which the next would reuse): the next makes a new one, + and its number is not closed again when the endpoint is */ + closesocket(ep->socket); + ep->socket = INVALID_SOCKET; + SET_FLAG(ep->flags, _transport_endpoint_nonblocking_bit, FALSE); } else { diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 3937d6f4..72edfb94 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -3825,6 +3825,13 @@ void game_engine_player_killed( killer (port/linux/game/network_distributed.c) */ network_distributed_player_killed(&killing_player_index, &killing_object_index, dead_player_index, &friendly_fire); + /* the host's kill of a player who quit, ahead of this client's clock + (game_update_quit_players has not come to its time yet) */ + if (network_game_distributed_client() && dead_player->quit_out_of_game_time != NONE && + killing_player_index == dead_player_index) + { + dead_player->quit_out_of_game = TRUE; + } dead_player->death_time = game_time_get(); if (game_engine->player_killed_player) { diff --git a/source/game/players.c b/source/game/players.c index 779bcb5f..9ebaf447 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -3850,10 +3850,15 @@ void players_update_after_game( telefrag_ticks = player->telefrag_timeout; if (telefrag_ticks >= 90) { + /* a client's view of who blocks is a latency late: the + host's kill arrives with its damage events (the message + shown once) */ + boolean client = network_game_distributed_client(); + if (player->unit_index != NONE) { unit = unit_get(player->unit_index); - if (!TEST_FLAG(unit->object.damage_flags, _object_die_act_of_god_bit)) + if (client ? telefrag_ticks == 90 : !TEST_FLAG(unit->object.damage_flags, _object_die_act_of_god_bit)) { if (player->local_player_index != NONE) { @@ -3870,9 +3875,12 @@ void players_update_after_game( } player_telefrag_effect_stop(iterator.datum_index); - unit_kill(player->unit_index); + if (!client) + unit_kill(player->unit_index); } } + if (client) + player_telefrag_effect_stop(iterator.datum_index); } else { diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 0d34f99d..b44130ed 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -2312,11 +2312,12 @@ static struct widget_instance *ui_widget_launch_widget( struct widget_instance *new_widget; short local_player_index; - /* port: the multiplayer menus open only on maps of a build that plays - multiplayer with the others (cache_files.c, cache_files_multiplayer_region); - otherwise the player is told why, and the main menu stays */ + /* port: the menus of multiplayer with other machines (not split screen's + or co-op's) open only on maps of a build that plays multiplayer with the + others (cache_files.c, cache_files_multiplayer_region); otherwise the + player is told why, and the menu stays */ { - static char const multiplayer_menus[] = "ui\\shell\\main_menu\\multiplayer_type_select\\"; + static char const multiplayer_menus[] = "ui\\shell\\main_menu\\multiplayer_type_select\\connected\\"; char const *name = tag_get_name(new_widget_tag_index); char build[0x20]; diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index a12674d5..5e04042a 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -5551,7 +5551,8 @@ static boolean multiplayer_level_select( { char build[0x20]; - if (global_network_game_server_get() && !cache_files_map_plays_multiplayer(map_name, build)) + if (global_network_game_server_get() && !network_game_is_splitscreen_local() && + !cache_files_map_plays_multiplayer(map_name, build)) { cache_files_show_multiplayer_unavailable(map_name, build); return FALSE; diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index 3837ecf0..2fef146a 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -1261,9 +1261,13 @@ boolean network_game_client_game_settings_updated( /* port: a map of a build this version does not play with others (its objects would not be the host's): said, and the game left */ - if (!cache_files_map_plays_multiplayer(message_packet->map.name, build)) + if (!network_game_is_splitscreen_local() && + !cache_files_map_plays_multiplayer(message_packet->map.name, build)) { cache_files_show_multiplayer_unavailable(message_packet->map.name, build); + /* (the menu's error the join's, not the connection lost that + the failure would otherwise give) */ + display_error_when_main_menu_loaded(_error_network_failed_to_join_game); return FALSE; } network_event("precaching map '%s'...", message_packet->map.name); diff --git a/source/networking/network_connection.c b/source/networking/network_connection.c index 2c630b5b..d20f0a2c 100644 --- a/source/networking/network_connection.c +++ b/source/networking/network_connection.c @@ -1896,10 +1896,13 @@ static void network_connection_flush_reliable_last( { unsigned long start_time = system_milliseconds(); + /* (and not at all for a peer that has taken nothing lately: gone, its + connection dropped for it) */ while (connection->reliable_outgoing_queue && circular_queue_size(connection->reliable_outgoing_queue) > 0 && network_connection_flush_reliable(connection) && circular_queue_size(connection->reliable_outgoing_queue) > 0 && - system_milliseconds() - start_time < 500) + system_milliseconds() - start_time < 500 && + system_milliseconds() - connection->reliable_outgoing_time < 100) { Sleep(5); } diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 0b789d04..c5d8f216 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -779,6 +779,71 @@ static long network_game_server_frequent_updates_until[MAXIMUM_NETWORK_MACHINE_C network_game_server_client_machine_may_slow_countdown) */ static boolean network_game_server_countdown_slowed[MAXIMUM_NETWORK_MACHINE_COUNT]; +/* port: the players added to the game in progress from each client +machine's address (a machine joins from one address, and joining again +drops the old: network_game_server_accept_client_machine_into_game), no +more than MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS a game, so that players +added and removed, or a machine joining again and again, do not take +every player of the game (a player gone stays for its scores) */ +enum +{ + MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS = 2 * MAXIMUM_PLAYERS_PER_MACHINE, +}; +static struct +{ + unsigned long address; + short count; +} network_game_server_ingame_additions[MAXIMUM_NETWORK_PLAYER_COUNT]; + +static short *network_game_server_ingame_addition_count( + unsigned long address, + boolean create) +{ + long index; + + if (!address) + return NULL; + for (index = 0; index < (long)NUMBEROF(network_game_server_ingame_additions); index++) + { + if (network_game_server_ingame_additions[index].address == address) + return &network_game_server_ingame_additions[index].count; + } + /* (more addresses than players: the game is full anyway) */ + for (index = 0; create && index < (long)NUMBEROF(network_game_server_ingame_additions); index++) + { + if (!network_game_server_ingame_additions[index].address) + { + network_game_server_ingame_additions[index].address = address; + network_game_server_ingame_additions[index].count = 0; + return &network_game_server_ingame_additions[index].count; + } + } + return NULL; +} + +/* (a client machine's player queued to add in game: one refused is as one +the game has no room for) */ +static boolean network_game_server_machine_may_add_player_ingame( + struct network_game_server *server, + long machine_index) +{ + short *count; + + if (!VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) || + network_game_server_client_machine_is_local(server, &server->client_machines[machine_index])) + { + return TRUE; + } + count = network_game_server_ingame_addition_count(network_game_server_client_machine_addresses[machine_index], FALSE); + if (count && *count >= MAXIMUM_INGAME_ADDITIONS_PER_ADDRESS) + { + network_event("client machine #%ld added too many players in game", machine_index); + return FALSE; + } + + return TRUE; +} + /* ---------- public code */ struct network_game_server *network_game_server_create( @@ -963,11 +1028,23 @@ static boolean network_game_server_network_lost( { static unsigned long down_time; static unsigned long checked_time; + static boolean asked; + static boolean available_asked; + static unsigned long asked_time; unsigned long now = system_milliseconds(); boolean in_game = server->state == _network_game_server_state_ingame || server->state == _network_game_server_state_postgame; - boolean available = transport_network_available(); boolean recent = checked_time && now - checked_time <= 2000; + boolean available; + + /* (asked once a second: each asking lists the machine's interfaces) */ + if (!asked || now - asked_time >= 1000) + { + available_asked = transport_network_available(); + asked_time = now; + asked = TRUE; + } + available = available_asked; checked_time = now; if (available) @@ -1969,11 +2046,20 @@ void network_game_server_update_ticks( } if (client_machine && + network_game_server_machine_may_add_player_ingame(server, client_machine_index) && network_game_server_add_player_to_game( server, client_machine, &server->queued_player)) { + if (!network_game_server_client_machine_is_local(server, client_machine)) + { + short *count = network_game_server_ingame_addition_count( + network_game_server_client_machine_addresses[client_machine_index], TRUE); + + if (count) + (*count)++; + } if (!network_game_server_send_player_joined_info_ingame( server, &server->queued_player)) @@ -3941,6 +4027,7 @@ boolean network_game_server_reset_to_pregame( server->sent_start_game_message = FALSE; server->queued_player_valid = FALSE; server->waiting_player_count = 0; + csmemset(network_game_server_ingame_additions, 0, sizeof(network_game_server_ingame_additions)); /* Preserve January's 32-bit wrap without overflowing signed arithmetic. * VC7 converts the unsigned result back to the same signed bit pattern. */ diff --git a/source/objects/damage.c b/source/objects/damage.c index 90ea9bda..d73a7e2d 100644 --- a/source/objects/damage.c +++ b/source/objects/damage.c @@ -147,7 +147,7 @@ boolean network_damage_replaying_kill(void); void network_damage_player_effect(long player_index, struct damage_data const *damage, real total_damage); void network_damage_aftermath(long object_index, struct damage_data const *damage, unsigned long being_damaged_flags, real shield_damage, real body_damage, real body_damage_multiplier, short body_part, short node_index, - short region_index, short material_index); + short region_index, short material_index, long victim_player_index); /* set while a distributed client carries out a kill it does not decide (an act of god: the host's word, network_distributed.c, or the world's) */ @@ -1373,6 +1373,7 @@ void object_cause_damage( struct object_datum *current_object; struct damage_resistance_material const *damage_material; long current_object_index; + long victim_player_index; unsigned long being_damaged_flags; real shield_damage; real body_damage; @@ -1790,6 +1791,13 @@ void object_cause_damage( } } + /* (the player of the unit, which a killing blow's aftermath + takes from it, unit_died) */ + { + struct unit_datum *victim = unit_try_and_get(current_object_index); + + victim_player_index = victim ? victim->unit.player_index : NONE; + } object_damage_aftermath( current_object_index, damage, @@ -1809,7 +1817,8 @@ void object_cause_damage( body_part, current_object_index == object_index ? node_index : NONE, current_object_index == object_index ? region_index : NONE, - current_object_index == object_index ? material_index : NONE); + current_object_index == object_index ? material_index : NONE, + victim_player_index); if (TEST_FLAG( being_damaged_flags, _object_being_damaged_body_destroyed_bit))