diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index ab2abb6b..c7f2866d 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -53,6 +53,9 @@ if(BUILD_TESTING) target_include_directories(netplay_sim_pad_cache_test PRIVATE include) target_compile_definitions(netplay_sim_pad_cache_test PRIVATE PSX_MAX_PLAYERS=4) add_test(NAME netplay_sim_pad_cache_test COMMAND netplay_sim_pad_cache_test) + add_executable(lobby_launch_transport_test tests/test_lobby_launch_transport.c) + target_include_directories(lobby_launch_transport_test PRIVATE include) + add_test(NAME lobby_launch_transport_test COMMAND lobby_launch_transport_test) add_executable(netplay_local_view_test tests/test_netplay_local_view.c) target_include_directories(netplay_local_view_test PRIVATE include) add_test(NAME netplay_local_view_test COMMAND netplay_local_view_test) diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index 4302990d..6881841a 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -272,6 +272,10 @@ typedef struct PsxLobbyJoinInfo { * force_input_relay is 0. The host binds bind_hostport and accepts the * guest (2 seats) or hubs (3+). */ int transport_host; + /* Launch: transport "ice" -- a 2-player room started on ICE + * (peer-to-peer, signalled over the lobby). No endpoint is dialled and + * force_input_relay is 0 (psx_lobby_launch_transport.h). */ + int transport_ice; /* A server code (need_password | bad_password | …) or, from * psx_lobby_set_last_error, one sentence the launcher shows as is. */ char last_error[192]; diff --git a/runtime/include/psx_lobby_launch_transport.h b/runtime/include/psx_lobby_launch_transport.h new file mode 100644 index 00000000..1d2e8de8 --- /dev/null +++ b/runtime/include/psx_lobby_launch_transport.h @@ -0,0 +1,59 @@ +/* psx_lobby_launch_transport.h - how a lobby `launch` is carried. + * + * The lobby server picks the transport at `start` (recomp-net-server + * start_transport, WS_LOBBY.md "Host relay"): + * "host" -- the host carries the match on host_endpoint; guests dial it. + * "ice" -- a 2-player room the host relay cannot carry starts on ICE: + * peer-to-peer over STUN/TURN, signalled over the lobby socket. + * The endpoints are not dialled at all. + * a relay_endpoint, or equal host/guest endpoints from a server that + * rewrote both to its relay advertise address -- legacy server SFU. + * + * The last rule used to win over "ice": two peers behind one NAT advertise + * the same public endpoint, so an ICE launch between them looked like a relay + * and each peer dialled its own address -- the match never linked. */ +#ifndef PSX_LOBBY_LAUNCH_TRANSPORT_H +#define PSX_LOBBY_LAUNCH_TRANSPORT_H + +#include +#include + +enum { + PSX_LAUNCH_DIRECT = 0, /* dial the peer endpoint (legacy p2p UDP) */ + PSX_LAUNCH_HOST = 1, /* host relay: host binds, guests dial host_endpoint */ + PSX_LAUNCH_ICE = 2, /* ICE peer-to-peer, signalled over the lobby */ + PSX_LAUNCH_RELAY = 3 /* server input relay (SFU) */ +}; + +static inline int psx_lobby_endpoint_usable(const char *ep) +{ + const char *colon; + if (!ep || !ep[0]) return 0; + colon = strrchr(ep, ':'); + if (!colon || !colon[1]) return 0; + return strtoul(colon + 1, NULL, 10) != 0; +} + +/* transport: the launch's "transport" field ("" when absent). + * relay_endpoint: the launch's relay_endpoint ("" when absent). + * caps_force_relay: the host's match_caps asked for the server relay. + * my_bind: this peer's own advertised bind ("" when unknown). */ +static inline int psx_lobby_launch_transport(const char *transport, + const char *relay_endpoint, + int caps_force_relay, + const char *host_ep, + const char *guest_ep, + const char *my_bind) +{ + if (transport && strcmp(transport, "host") == 0) return PSX_LAUNCH_HOST; + if (transport && strcmp(transport, "ice") == 0) return PSX_LAUNCH_ICE; + if (psx_lobby_endpoint_usable(relay_endpoint) || caps_force_relay) + return PSX_LAUNCH_RELAY; + if (psx_lobby_endpoint_usable(host_ep) && psx_lobby_endpoint_usable(guest_ep) && + strcmp(host_ep, guest_ep) == 0 && + (!my_bind || !my_bind[0] || strcmp(host_ep, my_bind) != 0)) + return PSX_LAUNCH_RELAY; + return PSX_LAUNCH_DIRECT; +} + +#endif diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index e74a632f..3d8de9cb 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -15236,13 +15236,14 @@ namespace { * §108's "always SFU"). Prefer caps.force_input_relay from the * relay_endpoint rewrite; also infer when host==guest advertise. */ out->force_input_relay = - (g_lnch_hosting_lan || g_lnch_joined_lan || ji->transport_host) + (g_lnch_hosting_lan || g_lnch_joined_lan || ji->transport_host || ji->transport_ice) ? 0 : (caps->force_input_relay ? 1 : 0); #if defined(RECOMP_LAUNCHER_HAS_HOST_RELAY) out->transport_host = ji->transport_host ? 1 : 0; #endif - if (!out->force_input_relay && !ji->transport_host && !g_lnch_hosting_lan && + if (!out->force_input_relay && !ji->transport_host && !ji->transport_ice && + !g_lnch_hosting_lan && !g_lnch_joined_lan && ji->host_endpoint[0] && ji->guest_endpoint[0] && std::strcmp(ji->host_endpoint, ji->guest_endpoint) == 0) { diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index 64fc252c..2b9c11d5 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -1,4 +1,5 @@ #include "psx_lobby_client.h" +#include "psx_lobby_launch_transport.h" #include "netplay_bios_settle.h" #include @@ -1910,16 +1911,12 @@ static void copy_first_usable_endpoint(char *dst, size_t dst_len, const char *a, static int using_server_input_relay(const PsxLobbyJoinInfo *j) { - if (g_lc.match_caps.valid && g_lc.match_caps.force_input_relay) - return 1; - /* Server rewrote both endpoints to the same relay advertise address. */ - if (j && j->host_endpoint[0] && j->guest_endpoint[0] && - !endpoint_port_is_zero(j->host_endpoint) && - !endpoint_port_is_zero(j->guest_endpoint) && - strcmp(j->host_endpoint, j->guest_endpoint) == 0 && - (!g_lc.my_bind[0] || strcmp(j->host_endpoint, g_lc.my_bind) != 0)) - return 1; - return 0; + if (!j) + return g_lc.match_caps.valid && g_lc.match_caps.force_input_relay; + return psx_lobby_launch_transport(j->transport_ice ? "ice" : j->transport_host ? "host" : "", + "", g_lc.match_caps.valid && g_lc.match_caps.force_input_relay, + j->host_endpoint, j->guest_endpoint, + g_lc.my_bind) == PSX_LAUNCH_RELAY; } static void fill_peer_bind_from_join(void) @@ -3275,6 +3272,7 @@ static void handle_server_json(const char *json) transport_kind[0] = '\0'; json_get_str(json, "transport", transport_kind, sizeof(transport_kind)); g_lc.join.transport_host = strcmp(transport_kind, "host") == 0 ? 1 : 0; + g_lc.join.transport_ice = strcmp(transport_kind, "ice") == 0 ? 1 : 0; rnet_host_relay_release_port(g_host_relay); g_lc.join.player_count = json_get_int(json, "player_count", g_lc.join.player_count); g_lc.join.max_slots = json_get_int(json, "max_slots", g_lc.join.max_slots); @@ -3283,7 +3281,10 @@ static void handle_server_json(const char *json) /* Prefer explicit relay_endpoint when the server opened input relay. * Apply after caps ingest: omitted force_input_relay must not leave * hosts on the hub path while guests dial the relay. */ - if (relay_endpoint[0] && !endpoint_port_is_zero(relay_endpoint)) { + if (g_lc.join.transport_ice) { + /* ICE: nothing is dialled; a stale relay ask must not survive. */ + if (g_lc.match_caps.valid) g_lc.match_caps.force_input_relay = 0; + } else if (relay_endpoint[0] && !endpoint_port_is_zero(relay_endpoint)) { char relay_raw[PSX_LOBBY_ENDPOINT_LEN]; strncpy(relay_raw, relay_endpoint, sizeof(relay_raw) - 1); relay_raw[sizeof(relay_raw) - 1] = '\0'; @@ -3325,7 +3326,11 @@ static void handle_server_json(const char *json) json_get_str(json, "transport", transport, sizeof(transport)); if (strcmp(transport, "ice_p2p") == 0) ice_p2p = 1; - if (force_relay) { + if (g_lc.join.transport_ice) { + fprintf(stderr, "psx_lobby: launch transport=ice (peer-to-peer, " + "signalled over the lobby)\n"); + fflush(stderr); + } else if (force_relay) { if (peer_bad) { strncpy(g_lc.join.last_error, "missing_endpoints", sizeof(g_lc.join.last_error) - 1); diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index d4a912ff..d1cbd122 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -3800,6 +3800,19 @@ static int resolve_use_ice(const PsxNetplayConfig *cfg) fflush(stderr); return 0; } +#if defined(PSX_HAS_LOBBY_CLIENT) && defined(RNET_ENABLE_ICE) + /* The server started this room on ICE (2 players the host relay cannot + * carry): peer-to-peer over STUN/TURN, signalled over the lobby socket. + * Same-NAT peers advertise equal endpoints; that is not a relay. */ + if (in_motk_room && !cfg->force_input_relay) { + const PsxLobbyJoinInfo *ji = psx_lobby_join_info(); + if (ji && ji->transport_ice) { + fprintf(stderr, "psx_netplay: lobby launch transport=ice — ICE peer-to-peer\n"); + fflush(stderr); + return 1; + } + } +#endif if (cfg->force_input_relay || in_motk_room) { if (!cfg->peer_hostport || !cfg->peer_hostport[0]) { fprintf(stderr, diff --git a/runtime/tests/test_lobby_launch_transport.c b/runtime/tests/test_lobby_launch_transport.c new file mode 100644 index 00000000..44ab8dda --- /dev/null +++ b/runtime/tests/test_lobby_launch_transport.c @@ -0,0 +1,31 @@ +#include "psx_lobby_launch_transport.h" +#undef NDEBUG +#include + +int main(void) +{ + /* ICE launch between two peers behind one NAT: same public endpoint on + * both sides must stay ICE, not turn into a relay to ourselves. */ + assert(psx_lobby_launch_transport("ice", "", 0, "136.25.29.198:7778", + "136.25.29.198:7778", "") == PSX_LAUNCH_ICE); + assert(psx_lobby_launch_transport("ice", "", 0, "1.2.3.4:7777", "5.6.7.8:7777", "") + == PSX_LAUNCH_ICE); + /* Host relay is unchanged, even with equal endpoints. */ + assert(psx_lobby_launch_transport("host", "", 0, "1.2.3.4:7777", "1.2.3.4:7777", "") + == PSX_LAUNCH_HOST); + /* Legacy server relay: explicit relay_endpoint, caps, or equal rewritten + * endpoints with no transport field. */ + assert(psx_lobby_launch_transport("", "9.9.9.9:8777", 0, "", "", "") == PSX_LAUNCH_RELAY); + assert(psx_lobby_launch_transport("", "", 1, "1.2.3.4:7777", "5.6.7.8:7777", "") + == PSX_LAUNCH_RELAY); + assert(psx_lobby_launch_transport("", "", 0, "9.9.9.9:8777", "9.9.9.9:8777", "") + == PSX_LAUNCH_RELAY); + /* ...but not our own bind echoed back, and not unusable ports. */ + assert(psx_lobby_launch_transport("", "", 0, "1.2.3.4:7777", "1.2.3.4:7777", + "1.2.3.4:7777") == PSX_LAUNCH_DIRECT); + assert(psx_lobby_launch_transport("", "", 0, "1.2.3.4:0", "1.2.3.4:0", "") + == PSX_LAUNCH_DIRECT); + assert(psx_lobby_launch_transport("", "", 0, "1.2.3.4:7777", "5.6.7.8:7777", "") + == PSX_LAUNCH_DIRECT); + return 0; +}