-
-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Renetcode: disconnect clients that are sending ConnectionRequests #136
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ struct Connection { | |
user_data: [u8; NETCODE_USER_DATA_BYTES], | ||
addr: SocketAddr, | ||
last_packet_received_time: Duration, | ||
connection_started_time: Duration, | ||
last_packet_send_time: Duration, | ||
timeout_seconds: i32, | ||
sequence: u64, | ||
|
@@ -338,6 +339,7 @@ impl NetcodeServer { | |
client_id: connect_token.client_id, | ||
last_packet_received_time: self.current_time, | ||
last_packet_send_time: self.current_time, | ||
connection_started_time: self.current_time, | ||
addr, | ||
state: ConnectionState::PendingResponse, | ||
send_key: connect_token.server_to_client_key, | ||
|
@@ -406,38 +408,59 @@ impl NetcodeServer { | |
); | ||
|
||
client.last_packet_received_time = self.current_time; | ||
match client.state { | ||
ConnectionState::Connected => match packet { | ||
Packet::Disconnect => { | ||
client.state = ConnectionState::Disconnected; | ||
if client.state != ConnectionState::Connected { | ||
return Ok(ServerResult::None); | ||
} | ||
|
||
match packet { | ||
Packet::Disconnect => { | ||
let client_id = client.client_id; | ||
self.clients[slot] = None; | ||
log::trace!("Client {} requested to disconnect", client_id); | ||
return Ok(ServerResult::ClientDisconnected { | ||
client_id, | ||
addr, | ||
payload: None, | ||
}); | ||
} | ||
Packet::Payload(payload) => { | ||
if !client.confirmed { | ||
log::trace!("Confirmed connection for Client {}", client.client_id); | ||
client.confirmed = true; | ||
} | ||
return Ok(ServerResult::Payload { | ||
client_id: client.client_id, | ||
payload, | ||
}); | ||
} | ||
Packet::KeepAlive { .. } => { | ||
if !client.confirmed { | ||
log::trace!("Confirmed connection for Client {}", client.client_id); | ||
client.confirmed = true; | ||
} | ||
return Ok(ServerResult::None); | ||
} | ||
Packet::ConnectionRequest { .. } => { | ||
// If a ConnectionRequest is received while connected, | ||
// this might be a client trying to rejoin | ||
Comment on lines
+443
to
+445
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this branch can actually run in practice for an honest client, since However, this branch can be exploited by a malicious party to force-disconnect clients using packet-sniffed client connection requests. The function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you are correct Hmm, would decoding the private connect token and checking that it is valid and from the same client be enough? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because the attacker is free to use an old connection request from the user. You could perhaps include a counter inside the connect token (authenticated as part of the private connect token), and auto-disconnect if a higher-generation token shows up for the same client. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lucaspoffo an alternative to a counter would be to use the token's One important detail: the server must emit a disconnect event for a reconnecting client so the server can properly reinitialize them. This general approach is not strictly as secure as the default netcode protocol. If clients request multiple new connect tokens from the backend, e.g. on a timer while disconnected, then they may reconnect with an older connect token while a newer connect token is only transmitted and cached. If an attacker somehow acquires the newer token then they can use it to force-disconnect the client. However it is better than blindly disconnecting clients, because usually clients will receive connect tokens over a secure channel (and only the renet client -> renet server initial connection is insecure). |
||
|
||
// We check the time when the connection started to detect rejoins | ||
const REJOIN_TIMEOUT: Duration = Duration::from_secs(10); | ||
if client.connection_started_time + REJOIN_TIMEOUT < self.current_time { | ||
let client_id = client.client_id; | ||
self.clients[slot] = None; | ||
log::trace!("Client {} requested to disconnect", client_id); | ||
log::trace!("Client {} is trying to rejoin", client_id); | ||
|
||
// Disconnect client to restart the connection process | ||
return Ok(ServerResult::ClientDisconnected { | ||
client_id, | ||
addr, | ||
payload: None, | ||
}); | ||
} | ||
Packet::Payload(payload) => { | ||
if !client.confirmed { | ||
log::trace!("Confirmed connection for Client {}", client.client_id); | ||
client.confirmed = true; | ||
} | ||
return Ok(ServerResult::Payload { | ||
client_id: client.client_id, | ||
payload, | ||
}); | ||
} | ||
Packet::KeepAlive { .. } => { | ||
if !client.confirmed { | ||
log::trace!("Confirmed connection for Client {}", client.client_id); | ||
client.confirmed = true; | ||
} | ||
return Ok(ServerResult::None); | ||
} | ||
_ => return Ok(ServerResult::None), | ||
}, | ||
|
||
return Ok(ServerResult::None); | ||
} | ||
_ => return Ok(ServerResult::None), | ||
} | ||
} | ||
|
@@ -738,12 +761,44 @@ mod tests { | |
|
||
#[test] | ||
fn server_connection() { | ||
fn client_connect(client: &mut NetcodeClient, client_addr: SocketAddr, user_data: [u8; 256], server: &mut NetcodeServer) { | ||
assert!(!client.is_connected()); | ||
let (client_packet, _) = client.update(NETCODE_SEND_RATE).unwrap(); | ||
|
||
let result = server.process_packet(client_addr, client_packet); | ||
assert!(matches!(result, ServerResult::PacketToSend { .. })); | ||
match result { | ||
ServerResult::PacketToSend { payload, .. } => client.process_packet(payload), | ||
_ => unreachable!(), | ||
}; | ||
|
||
assert!(!client.is_connected()); | ||
let (client_packet, _) = client.update(Duration::ZERO).unwrap(); | ||
let result = server.process_packet(client_addr, client_packet); | ||
|
||
match result { | ||
ServerResult::ClientConnected { | ||
client_id: r_id, | ||
user_data: r_data, | ||
payload, | ||
.. | ||
} => { | ||
assert_eq!(client.client_id(), r_id); | ||
assert_eq!(user_data, *r_data); | ||
client.process_packet(payload) | ||
} | ||
_ => unreachable!(), | ||
}; | ||
|
||
assert!(client.is_connected()); | ||
} | ||
|
||
let mut server = new_server(); | ||
let server_addresses: Vec<SocketAddr> = server.addresses(); | ||
let user_data = generate_random_bytes(); | ||
let expire_seconds = 3; | ||
let client_id = 4; | ||
let timeout_seconds = 5; | ||
let expire_seconds = 500; | ||
let client_id: u64 = 4; | ||
let timeout_seconds = 30; | ||
let client_addr: SocketAddr = "127.0.0.1:3000".parse().unwrap(); | ||
let connect_token = ConnectToken::generate( | ||
Duration::ZERO, | ||
|
@@ -757,35 +812,9 @@ mod tests { | |
) | ||
.unwrap(); | ||
let client_auth = ClientAuthentication::Secure { connect_token }; | ||
let mut client = NetcodeClient::new(Duration::ZERO, client_auth).unwrap(); | ||
let (client_packet, _) = client.update(Duration::ZERO).unwrap(); | ||
|
||
let result = server.process_packet(client_addr, client_packet); | ||
assert!(matches!(result, ServerResult::PacketToSend { .. })); | ||
match result { | ||
ServerResult::PacketToSend { payload, .. } => client.process_packet(payload), | ||
_ => unreachable!(), | ||
}; | ||
|
||
assert!(!client.is_connected()); | ||
let (client_packet, _) = client.update(Duration::ZERO).unwrap(); | ||
let result = server.process_packet(client_addr, client_packet); | ||
let mut client = NetcodeClient::new(Duration::ZERO, client_auth.clone()).unwrap(); | ||
|
||
match result { | ||
ServerResult::ClientConnected { | ||
client_id: r_id, | ||
user_data: r_data, | ||
payload, | ||
.. | ||
} => { | ||
assert_eq!(client_id, r_id); | ||
assert_eq!(user_data, *r_data); | ||
client.process_packet(payload) | ||
} | ||
_ => unreachable!(), | ||
}; | ||
|
||
assert!(client.is_connected()); | ||
client_connect(&mut client, client_addr, user_data, &mut server); | ||
|
||
for _ in 0..3 { | ||
let payload = [7u8; 300]; | ||
|
@@ -818,6 +847,19 @@ mod tests { | |
} | ||
|
||
assert!(server.is_client_connected(client_id)); | ||
|
||
// Test rejoin | ||
server.update(Duration::from_secs(15)); // Client should be timing out | ||
|
||
// Create new client with same connect token | ||
let mut client = NetcodeClient::new(Duration::ZERO, client_auth).unwrap(); | ||
let (client_packet, _) = client.update(Duration::ZERO).unwrap(); | ||
let result = server.process_packet(client_addr, client_packet); | ||
|
||
// The client should be disconnected now and the connection workflow should restart | ||
assert!(matches!(result, ServerResult::ClientDisconnected { payload: None, .. })); | ||
client_connect(&mut client, client_addr, user_data, &mut server); | ||
|
||
let result = server.disconnect(client_id); | ||
match result { | ||
ServerResult::ClientDisconnected { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line was removed:
client.state = ConnectionState::Disconnected;