From de30f212bc45f5ef68087e672d43815afb1ea788 Mon Sep 17 00:00:00 2001 From: Cthulhu-throwaway <96153783+Cthulhu-throwaway@users.noreply.github.com> Date: Mon, 30 May 2022 05:23:39 -0300 Subject: [PATCH] (Netplay) Enforce a timeout during connection --- libretro-common/include/net/net_socket.h | 16 +- libretro-common/net/net_socket.c | 279 +++++++++++++++++++---- network/netplay/netplay_frontend.c | 163 ++++++------- 3 files changed, 318 insertions(+), 140 deletions(-) diff --git a/libretro-common/include/net/net_socket.h b/libretro-common/include/net/net_socket.h index 2bf0b33378..c32a2d6504 100644 --- a/libretro-common/include/net/net_socket.h +++ b/libretro-common/include/net/net_socket.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010-2020 The RetroArch team +/* Copyright (C) 2010-2022 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this file (net_socket.h). @@ -72,12 +72,20 @@ bool socket_nonblock(int fd); int socket_select(int nfds, fd_set *readfs, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); -int socket_send_all_blocking(int fd, const void *data_, size_t size, bool no_signal); +bool socket_send_all_blocking(int fd, const void *data_, size_t size, bool no_signal); + +bool socket_send_all_blocking_with_timeout(int fd, + const void *data_, size_t size, + unsigned timeout, bool no_signal); ssize_t socket_send_all_nonblocking(int fd, const void *data_, size_t size, bool no_signal); -int socket_receive_all_blocking(int fd, void *data_, size_t size); +bool socket_receive_all_blocking(int fd, void *data_, size_t size); + +bool socket_receive_all_blocking_with_timeout(int fd, + void *data_, size_t size, + unsigned timeout); ssize_t socket_receive_all_nonblocking(int fd, bool *error, void *data_, size_t size); @@ -86,6 +94,8 @@ bool socket_bind(int fd, void *data); int socket_connect(int fd, void *data, bool timeout_enable); +bool socket_connect_with_timeout(int fd, void *data, unsigned timeout); + int socket_create( const char *name, enum socket_domain domain_type, diff --git a/libretro-common/net/net_socket.c b/libretro-common/net/net_socket.c index c983bfe9af..fce8282a09 100644 --- a/libretro-common/net/net_socket.c +++ b/libretro-common/net/net_socket.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2010-2020 The RetroArch team +/* Copyright (C) 2010-2022 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this file (net_socket.c). @@ -31,6 +31,8 @@ #include #endif +#include + #include #include @@ -90,38 +92,89 @@ int socket_next(void **addrinfo) ssize_t socket_receive_all_nonblocking(int fd, bool *error, void *data_, size_t size) { - const uint8_t *data = (const uint8_t*)data_; - ssize_t ret = recv(fd, (char*)data, size, 0); + ssize_t ret = recv(fd, (char*)data_, size, 0); if (ret > 0) return ret; - if (ret == 0) - { - /* Socket closed */ - *error = true; - return -1; - } - - if (isagain((int)ret)) + if (ret < 0 && isagain((int)ret)) return 0; *error = true; return -1; } -int socket_receive_all_blocking(int fd, void *data_, size_t size) +bool socket_receive_all_blocking(int fd, void *data_, size_t size) { const uint8_t *data = (const uint8_t*)data_; while (size) { ssize_t ret = recv(fd, (char*)data, size, 0); - if (ret <= 0) + + if (!ret) return false; - data += ret; - size -= ret; + if (ret < 0) + { + if (!isagain((int)ret)) + return false; + } + else + { + data += ret; + size -= ret; + } + } + + return true; +} + +bool socket_receive_all_blocking_with_timeout(int fd, + void *data_, size_t size, + unsigned timeout) +{ + const uint8_t *data = (const uint8_t*)data_; + retro_time_t deadline = cpu_features_get_time_usec(); + + if (timeout) + deadline += (retro_time_t)timeout * 1000000; + else + deadline += 5000000; + + while (size) + { + ssize_t ret = recv(fd, (char*)data, size, 0); + + if (!ret) + return false; + + if (ret < 0) + { + retro_time_t time_delta; + fd_set fds; + struct timeval tv; + + if (!isagain((int)ret)) + return false; + + time_delta = deadline - cpu_features_get_time_usec(); + + if (time_delta <= 0) + return false; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + tv.tv_sec = (unsigned)(time_delta / 1000000); + tv.tv_usec = (unsigned)(time_delta % 1000000); + if (socket_select(fd + 1, &fds, NULL, NULL, &tv) <= 0) + return false; + } + else + { + data += ret; + size -= ret; + } } return true; @@ -187,25 +240,80 @@ int socket_select(int nfds, fd_set *readfs, fd_set *writefds, #endif } -int socket_send_all_blocking(int fd, const void *data_, size_t size, +bool socket_send_all_blocking(int fd, const void *data_, size_t size, bool no_signal) { const uint8_t *data = (const uint8_t*)data_; + int flags = no_signal ? MSG_NOSIGNAL : 0; while (size) { - ssize_t ret = send(fd, (const char*)data, size, - no_signal ? MSG_NOSIGNAL : 0); - if (ret <= 0) + ssize_t ret = send(fd, (const char*)data, size, flags); + + if (!ret) + continue; + + if (ret < 0) { - if (isagain((int)ret)) - continue; - - return false; + if (!isagain((int)ret)) + return false; } + else + { + data += ret; + size -= ret; + } + } - data += ret; - size -= ret; + return true; +} + +bool socket_send_all_blocking_with_timeout(int fd, + const void *data_, size_t size, + unsigned timeout, bool no_signal) +{ + const uint8_t *data = (const uint8_t*)data_; + int flags = no_signal ? MSG_NOSIGNAL : 0; + retro_time_t deadline = cpu_features_get_time_usec(); + + if (timeout) + deadline += (retro_time_t)timeout * 1000000; + else + deadline += 5000000; + + while (size) + { + ssize_t ret = send(fd, (const char*)data, size, flags); + + if (!ret) + continue; + + if (ret < 0) + { + retro_time_t time_delta; + fd_set fds; + struct timeval tv; + + if (!isagain((int)ret)) + return false; + + time_delta = deadline - cpu_features_get_time_usec(); + + if (time_delta <= 0) + return false; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + tv.tv_sec = (unsigned)(time_delta / 1000000); + tv.tv_usec = (unsigned)(time_delta % 1000000); + if (socket_select(fd + 1, NULL, &fds, NULL, &tv) <= 0) + return false; + } + else + { + data += ret; + size -= ret; + } } return true; @@ -215,12 +323,15 @@ ssize_t socket_send_all_nonblocking(int fd, const void *data_, size_t size, bool no_signal) { const uint8_t *data = (const uint8_t*)data_; - ssize_t sent = 0; + int flags = no_signal ? MSG_NOSIGNAL : 0; while (size) { - ssize_t ret = send(fd, (const char*)data, size, - no_signal ? MSG_NOSIGNAL : 0); + ssize_t ret = send(fd, (const char*)data, size, flags); + + if (!ret) + break; + if (ret < 0) { if (isagain((int)ret)) @@ -228,15 +339,14 @@ ssize_t socket_send_all_nonblocking(int fd, const void *data_, size_t size, return -1; } - else if (ret == 0) - break; - - data += ret; - size -= ret; - sent += ret; + else + { + data += ret; + size -= ret; + } } - return sent; + return (ssize_t)((size_t)data - (size_t)data_); } bool socket_bind(int fd, void *data) @@ -266,37 +376,106 @@ int socket_connect(int fd, void *data, bool timeout_enable) timeout.tv_sec = 4; timeout.tv_usec = 0; - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof timeout); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); } -#endif -#if defined(GEKKO) && !defined(WIIU) +#elif defined(GEKKO) && !defined(WIIU) if (timeout_enable) { struct timeval timeout; timeout.tv_sec = 4; timeout.tv_usec = 0; - net_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof timeout); + net_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); } #endif -#if defined(WIIU) - int op = 1; - setsockopt(fd, SOL_SOCKET, SO_WINSCALE, &op, sizeof(op)); - if (addr->ai_socktype == SOCK_STREAM) { - setsockopt(fd, SOL_SOCKET, SO_TCPSACK, &op, sizeof(op)); - setsockopt(fd, SOL_SOCKET, 0x10000, &op, sizeof(op)); - int recvsz = WIIU_RCVBUF; - setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &recvsz, sizeof(recvsz)); - int sendsz = WIIU_SNDBUF; - setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sendsz, sizeof(sendsz)); +#ifdef WIIU + { + int op = 1; + + setsockopt(fd, SOL_SOCKET, SO_WINSCALE, &op, sizeof(op)); + + if (addr->ai_socktype == SOCK_STREAM) + { + int recvsz = WIIU_RCVBUF; + int sendsz = WIIU_SNDBUF; + + setsockopt(fd, SOL_SOCKET, SO_TCPSACK, &op, sizeof(op)); + setsockopt(fd, SOL_SOCKET, 0x10000, &op, sizeof(op)); + setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &recvsz, sizeof(recvsz)); + setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sendsz, sizeof(sendsz)); + } } - #endif return connect(fd, addr->ai_addr, addr->ai_addrlen); } +bool socket_connect_with_timeout(int fd, void *data, unsigned timeout) +{ + int res; + struct addrinfo *addr = (struct addrinfo*)data; + + if (!socket_nonblock(fd)) + return false; + +#ifdef WIIU + { + int op = 1; + + setsockopt(fd, SOL_SOCKET, SO_WINSCALE, &op, sizeof(op)); + + if (addr->ai_socktype == SOCK_STREAM) + { + int recvsz = WIIU_RCVBUF; + int sendsz = WIIU_SNDBUF; + + setsockopt(fd, SOL_SOCKET, SO_TCPSACK, &op, sizeof(op)); + setsockopt(fd, SOL_SOCKET, 0x10000, &op, sizeof(op)); + setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &recvsz, sizeof(recvsz)); + setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sendsz, sizeof(sendsz)); + } + } +#endif + + res = connect(fd, addr->ai_addr, addr->ai_addrlen); + if (res) + { + fd_set wfd, efd; + struct timeval tv = {0}; + + if (!isagain(res)) +#if !defined(_WIN32) && defined(EINPROGRESS) + if (errno != EINPROGRESS) +#else + return false; +#endif + + FD_ZERO(&wfd); + FD_ZERO(&efd); + FD_SET(fd, &wfd); + FD_SET(fd, &efd); + tv.tv_sec = timeout ? timeout : 5; + if (socket_select(fd + 1, NULL, &wfd, &efd, &tv) <= 0) + return false; + if (FD_ISSET(fd, &efd)) + return false; + } + +#ifdef SO_ERROR + { + int error = -1; + socklen_t errsz = sizeof(error); + + getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &errsz); + if (error) + return false; + } +#endif + + return true; +} + static int domain_get(enum socket_domain type) { switch (type) diff --git a/network/netplay/netplay_frontend.c b/network/netplay/netplay_frontend.c index 1e8cc5e828..b3044154ee 100644 --- a/network/netplay/netplay_frontend.c +++ b/network/netplay/netplay_frontend.c @@ -6396,7 +6396,7 @@ void netplay_deinit_nat_traversal(void) task_push_netplay_nat_close(&net_st->nat_traversal_request); } -static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, +static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *addr, bool is_server, bool is_mitm) { #ifndef HAVE_SOCKET_LEGACY @@ -6404,8 +6404,8 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, char host[256], port[6]; #endif const char *dmsg = NULL; - int fd = socket(res->ai_family, res->ai_socktype, - res->ai_protocol); + int fd = socket(addr->ai_family, addr->ai_socktype, + addr->ai_protocol); if (fd < 0) return -1; @@ -6415,17 +6415,19 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, if (!is_server) { - if (!socket_connect(fd, (void*)res, false)) + if (socket_connect_with_timeout(fd, (void*)addr, 10)) { /* If we are connecting to a tunnel server, we must also send our session linking request. */ - if (!netplay->mitm_session_id.magic || socket_send_all_blocking(fd, - &netplay->mitm_session_id, sizeof(netplay->mitm_session_id), true)) + if (!netplay->mitm_session_id.magic || + socket_send_all_blocking_with_timeout(fd, + &netplay->mitm_session_id, sizeof(netplay->mitm_session_id), + 5, true)) return fd; } #ifndef HAVE_SOCKET_LEGACY - if (!getnameinfo(res->ai_addr, res->ai_addrlen, + if (!getnameinfo(addr->ai_addr, addr->ai_addrlen, host, sizeof(host), port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV)) { @@ -6440,32 +6442,36 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, } else if (is_mitm) { - if (!socket_connect(fd, (void*)res, false)) + if (socket_connect_with_timeout(fd, (void*)addr, 10)) { - mitm_id_t new_session = {0}; - mitm_id_t invalid_session = {0}; + mitm_id_t new_session = {0}; /* To request a new session, we send the magic with the rest of the ID zeroed. */ new_session.magic = htonl(MITM_SESSION_MAGIC); /* Tunnel server should provide us with our session ID. */ - if (socket_send_all_blocking(fd, - &new_session, sizeof(new_session), true) && - socket_receive_all_blocking(fd, - &netplay->mitm_session_id, sizeof(netplay->mitm_session_id)) && - ntohl(netplay->mitm_session_id.magic) == MITM_SESSION_MAGIC && - memcmp(netplay->mitm_session_id.unique, invalid_session.unique, - sizeof(netplay->mitm_session_id.unique))) + if (socket_send_all_blocking_with_timeout(fd, + &new_session, sizeof(new_session), 5, true) && + socket_receive_all_blocking_with_timeout(fd, + &netplay->mitm_session_id, sizeof(netplay->mitm_session_id), + 5)) { - /* Initialize data for handling tunneled client connections. */ - netplay->mitm_pending = (struct netplay_mitm_pending*)calloc(1, sizeof(*netplay->mitm_pending)); - if (netplay->mitm_pending) + if (ntohl(netplay->mitm_session_id.magic) == MITM_SESSION_MAGIC && + memcmp(netplay->mitm_session_id.unique, new_session.unique, + sizeof(netplay->mitm_session_id.unique))) { - memset(netplay->mitm_pending->fds, -1, - sizeof(netplay->mitm_pending->fds)); - netplay->mitm_pending->addr = res; - return fd; + /* Initialize data for handling tunneled client connections. */ + netplay->mitm_pending = (struct netplay_mitm_pending*) + calloc(1, sizeof(*netplay->mitm_pending)); + if (netplay->mitm_pending) + { + memset(netplay->mitm_pending->fds, -1, + sizeof(netplay->mitm_pending->fds)); + netplay->mitm_pending->addr = addr; + + return fd; + } } } @@ -6474,7 +6480,7 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, else { #ifndef HAVE_SOCKET_LEGACY - if (!getnameinfo(res->ai_addr, res->ai_addrlen, + if (!getnameinfo(addr->ai_addr, addr->ai_addrlen, host, sizeof(host), port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV)) { @@ -6492,7 +6498,7 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, { #if defined(HAVE_INET6) && defined(IPV6_V6ONLY) /* Make sure we accept connections on both IPv6 and IPv4 */ - if (res->ai_family == AF_INET6) + if (addr->ai_family == AF_INET6) { int on = 0; if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, @@ -6500,15 +6506,16 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, RARCH_WARN("[Netplay] Failed to listen on both IPv6 and IPv4.\n"); } #endif - if (socket_bind(fd, (void*)res)) + + if (socket_bind(fd, (void*)addr)) { - if (!listen(fd, 64)) + if (!listen(fd, 64) && socket_nonblock(fd)) return fd; } else { #ifndef HAVE_SOCKET_LEGACY - if (!getnameinfo(res->ai_addr, res->ai_addrlen, + if (!getnameinfo(addr->ai_addr, addr->ai_addrlen, NULL, 0, port, sizeof(port), NI_NUMERICSERV)) { snprintf(msg, sizeof(msg), @@ -6533,13 +6540,16 @@ static int init_tcp_connection(netplay_t *netplay, const struct addrinfo *res, static bool init_tcp_socket(netplay_t *netplay, const char *server, const char *mitm, uint16_t port) { - struct addrinfo *res; - const struct addrinfo *tmp_info; char port_buf[6]; + const struct addrinfo *tmp_info; + struct addrinfo *addr = NULL; struct addrinfo hints = {0}; bool is_mitm = !server && mitm; int fd = -1; + if (!network_init()) + return false; + if (!server) { if (!is_mitm) @@ -6563,7 +6573,7 @@ static bool init_tcp_socket(netplay_t *netplay, snprintf(port_buf, sizeof(port_buf), "%hu", port); if (getaddrinfo_retro(is_mitm ? mitm : server, port_buf, - &hints, &res)) + &hints, &addr)) { if (!server && !is_mitm) { @@ -6571,7 +6581,7 @@ static bool init_tcp_socket(netplay_t *netplay, try_ipv4: /* Didn't work with IPv6, try IPv4 */ hints.ai_family = AF_INET; - if (getaddrinfo_retro(server, port_buf, &hints, &res)) + if (getaddrinfo_retro(server, port_buf, &hints, &addr)) #endif { RARCH_ERR("[Netplay] Failed to set a hosting address.\n"); @@ -6580,20 +6590,22 @@ try_ipv4: } else { - RARCH_ERR("[Netplay] Failed to resolve host: %s\n", is_mitm ? mitm : server); + RARCH_ERR("[Netplay] Failed to resolve host: %s\n", + is_mitm ? mitm : server); return false; } } - if (!res) + if (!addr) return false; /* If we're serving on IPv6, make sure we accept all connections, including * IPv4 */ #ifdef HAVE_INET6 - if (!server && !is_mitm && res->ai_family == AF_INET6) + if (!server && !is_mitm && addr->ai_family == AF_INET6) { - struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) res->ai_addr; + struct sockaddr_in6 *sin6 = (struct sockaddr_in6*)addr->ai_addr; + #if defined(_MSC_VER) && _MSC_VER <= 1200 IN6ADDR_SETANY(sin6); #else @@ -6604,7 +6616,7 @@ try_ipv4: /* If "localhost" is used, it is important to check every possible * address for IPv4/IPv6. */ - tmp_info = res; + tmp_info = addr; do { @@ -6614,10 +6626,10 @@ try_ipv4: } while ((tmp_info = tmp_info->ai_next)); if (netplay->mitm_pending && netplay->mitm_pending->addr) - netplay->mitm_pending->base_addr = res; + netplay->mitm_pending->base_addr = addr; else - freeaddrinfo_retro(res); - res = NULL; + freeaddrinfo_retro(addr); + addr = NULL; if (fd < 0) { @@ -6629,18 +6641,10 @@ try_ipv4: return false; } - if (!socket_nonblock(fd)) - { - socket_close(fd); - return false; - } - if (server) { netplay->connections[0].active = true; netplay->connections[0].fd = fd; - memset(&netplay->connections[0].addr, 0, - sizeof(netplay->connections[0].addr)); } else netplay->listen_fd = fd; @@ -6648,21 +6652,6 @@ try_ipv4: return true; } -static bool init_socket(netplay_t *netplay, - const char *server, const char *mitm, uint16_t port) -{ - if (!network_init()) - return false; - - if (!init_tcp_socket(netplay, server, mitm, port)) - return false; - - if (netplay->is_server && netplay->nat_traversal) - netplay_init_nat_traversal(netplay); - - return true; -} - static bool netplay_init_socket_buffers(netplay_t *netplay) { /* Make our packet buffer big enough for a save state and stall-frames-many @@ -6863,26 +6852,20 @@ netplay_t *netplay_new(const char *server, const char *mitm, uint16_t port, netplay->ext_tcp_port = port; netplay->cbs = *cb; netplay->is_server = !server; - netplay->is_connected = false; netplay->nat_traversal = (!server && !mitm) ? nat_traversal : false; netplay->stateless_mode = stateless_mode; netplay->check_frames = check_frames; - netplay->crc_validity_checked = false; netplay->crcs_valid = true; netplay->quirks = quirks; netplay->simple_rand_next = 1; netplay->self_mode = netplay->is_server ? - NETPLAY_CONNECTION_SPECTATING : - NETPLAY_CONNECTION_NONE; + NETPLAY_CONNECTION_SPECTATING : NETPLAY_CONNECTION_NONE; if (netplay->stateless_mode) netplay->quirks |= NETPLAY_QUIRK_NO_SAVESTATES; if (netplay->is_server) { - netplay->connections = NULL; - netplay->connections_size = 0; - netplay->allow_pausing = settings->bools.netplay_allow_pausing; netplay->input_latency_frames_min = @@ -6902,10 +6885,10 @@ netplay_t *netplay_new(const char *server, const char *mitm, uint16_t port, if (!string_is_empty(mitm_session)) { - int flen; - unsigned char *buf; + int flen = 0; + unsigned char *buf = + unbase64(mitm_session, strlen(mitm_session), &flen); - buf = unbase64(mitm_session, strlen(mitm_session), &flen); if (!buf) goto failure; if (flen != sizeof(netplay->mitm_session_id.unique)) @@ -6922,11 +6905,11 @@ netplay_t *netplay_new(const char *server, const char *mitm, uint16_t port, netplay->allow_pausing = true; } - strlcpy(netplay->nick, !string_is_empty(nick) - ? nick : RARCH_DEFAULT_NICK, - sizeof(netplay->nick)); + strlcpy(netplay->nick, + !string_is_empty(nick) ? nick : RARCH_DEFAULT_NICK, + sizeof(netplay->nick)); - if (!init_socket(netplay, server, mitm, port) || + if (!init_tcp_socket(netplay, server, mitm, port) || !netplay_init_buffers(netplay)) goto failure; @@ -6936,33 +6919,38 @@ netplay_t *netplay_new(const char *server, const char *mitm, uint16_t port, if (netplay->mitm_session_id.magic) { - int flen; - char *buf; - net_driver_state_t *net_st = &networking_driver_st; + net_driver_state_t *net_st = &networking_driver_st; struct netplay_room *host_room = &net_st->host_room; - - buf = base64(netplay->mitm_session_id.unique, + int flen = 0; + char *buf = base64(netplay->mitm_session_id.unique, sizeof(netplay->mitm_session_id.unique), &flen); + if (!buf) goto failure; - strlcpy(host_room->mitm_session, buf, sizeof(host_room->mitm_session)); free(buf); } + else if (netplay->nat_traversal) + netplay_init_nat_traversal(netplay); /* Clients get device info from the server */ for (i = 0; i < MAX_INPUT_DEVICES; i++) { uint32_t dtype = input_config_get_device(i); + netplay->config_devices[i] = dtype; - if ((dtype&RETRO_DEVICE_MASK) == RETRO_DEVICE_KEYBOARD) + + if ((dtype & RETRO_DEVICE_MASK) == RETRO_DEVICE_KEYBOARD) { netplay->have_updown_device = true; netplay_key_hton_init(); } - if (dtype != RETRO_DEVICE_NONE && !netplay_expected_input_size(netplay, 1<